Flint mandate engine — give your agent a mandate, not your keys (+ MKT-1 fix + 0.01% audit)#13
Open
SashaMIT wants to merge 93 commits into
Open
Flint mandate engine — give your agent a mandate, not your keys (+ MKT-1 fix + 0.01% audit)#13SashaMIT wants to merge 93 commits into
SashaMIT wants to merge 93 commits into
Conversation
Grade card (security 8, crypto 8, arch 7, code 7, product 6, perf 5; ~7 overall), mission derived from the code, and a fundability-ordered roadmap. Two framing corrections from the swarm: T7 is peripheral (the real crypto risk is the pre-1.0 ml-dsa/ml-kem crates); the 2-of-3 quorum is one trust domain today. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
…he KID→tokenId resolver
The resolver could mis-bind a buyer to a hostile co-channel mint (HIGH,
on-chain-reachable money path). Two holes, both closed:
- abi.rs: pick_asset_created_binding_kid → collect_kid_bindings now returns the
UNION of precise+substring binders (no 'prefer precise' preference), so a
hostile canonical mint can no longer displace a legit RELAYED (substring) mint.
- main.rs: resolve_token_id now accumulates every distinct (operative,tokenId)
across the WHOLE channel range (never returns on the first window) and binds
ONLY if exactly one exists, else returns ambiguous_kid_binding. Kills the
cross-window escape where a hostile newer-window mint was returned before the
victim's older window was scanned. Split-retry concatenates both halves;
early-exit at >=2 distinct binders bounds the ambiguous/griefing path.
- Fail-closed-by-omission hardening (red-team follow-up): a candidate whose mint
calldata cannot be fetched aborts the resolve rather than deciding on a partial
set (which could drop the legit binder and leave a hostile singleton).
Tradeoff: availability (a griefer can block a resolve by minting a same-channel
KID collision) for safety (funds are never bound to the wrong token) — the
correct money-path default. Adversarially red-teamed: all 6 mis-bind vectors
CONFIRMED-SAFE. Ratchets: abi::tests::{collect_kid_bindings_binds_the_unique_asset,
_surfaces_both_binders_in_one_window_so_caller_fails_closed,
kid_bindings_across_windows_fail_closed_on_cross_window_collision}.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
…mit benchmark NOW-sprint items #3 and #4 from the 0.01% audit. #3 (MintError): mint_asset/assemble_calldata return a typed MintError{Invalid, Upstream} that carries its HTTP class, deleting the fragile substring dispatch in viewer_open.rs (err.contains("MintAssembly")…) that silently flipped a 400 to a 502 when an error string was edited — on a money path. HTTP-agnostic by design (the gateway maps is_client_error()); Upstream detail is never leaked to the caller. Classification lives at the layer that owns run_chain_capsule's error contract, ratcheted by assemble_error_classification_maps_capsule_op_failure_to_client_error. #4 (first benchmark): benches/audit_emit.rs — hermetic std-timing harness (no criterion, no network) measuring the audit emit ceiling the SPEED grade rests on. Measured on a dev SSD: ~879k ops/s memory-only (1.14 us) vs ~789 ops/s file-backed with fsync-per-record (1267 us) — a ~1113x durable-custody tax and a ~789 durable-emit/s single-writer ceiling. Converts the KNOWN_GAPS estimate to a real number; group-commit batching K records/fsync would lift the ceiling to ~789xK. KNOWN_GAPS perf section + AUDIT_2026-07-03 roadmap updated with the measurement. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
…cture/UX verdict, vision, ordered roadmap Capstone of the 0.01% assessment (professionals + innovator + visionary + advisor + stakeholder lenses): overall grade 6.5/10 (8 on the security/crypto core, 5 on the UX + unbuilt ends), the architecture verdict (right primitives, trusted-core inversion is the #1 structural debt — sequence the restructure AFTER the wedge except halo-wiring), the UX verdict (one finished room, three design systems, a Web3-leaking money path), the Sealed-Rights Commerce vision + wedge, and one leverage-ordered master plan converging on: close one receipt-backed sale, then ship the Tier-3 demo. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
…pending Fable logic audit) Six-seat 0.01% panel (local-AI, agentic-product, creator-economy, tokenized- markets, securities/IP law, sovereign-systems) audit of the Flint/ElastOS vision, laid back with five corrections (frontier-rented-not-owned; broker-not-twin; already-cloned-not-everyone; royalty-market-is-a-GMV-derivative; promote Carrier peer-auth), the compliance must-be-trues, the tracked blind spots, and the first-principles fundamental-goal reframe (enforceable control over what you own, not 'everyone's data becomes capital'). NOTE: a Fable-model independent logic audit of this synthesis is in flight (per the founder's request that the strategy reasoning run on Fable, not Opus); this doc will be reconciled/updated with that audit's verdict before it is final. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
Independent Fable-model re-audit of the panel's logic. Kept the spine, folded in: - deeper bedrock: 'checkable accountability / delegation without abdication' (symmetric — owner + counterparty + regulator buy the same primitive), not just owner-side 'enforceable control' - sharper one-liner: 'Give your AI a mandate, not your keys' (retire 'trust layer') - declassification gate (remote model routing = a signed egress capability, not a silent router) - creator: sell the enforcement backend to intermediaries (unions/agencies), and 'data->capital' survives as pooled consented collective licensing - royalty: issuer-is-oracle conflict; ledger-now / market-later - four blind spots in the panel's own logic: no demand side, no dependency ordering (broker + lock dead-end without peer auth), unaudited 0 business model (fiduciary service => recurring/take-rate), regulation as a sellable wedge not just a limit - the one bet: one demo instantiating broker + lock + mesh in a single authenticated, metered, revocable cross-node transaction Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
…bility) + Knowledge Navigator interface Board audited to convergence (founder + operator converged on Agent PAM / accountable agent; dealmaker's data-licensing wedge killed at the physics level by the contrarian VC — a boundary voided by the transaction it enables). Final thesis: the wedge is authority OUT (agent mandates + admissible receipts for regulated finance, on the existing PAM budget), not assets IN (DRM = act two). Moat vs Okta/Entra/MCP = enforcement + admissible evidence at the execution layer, become the standard auditors name. Includes the Knowledge Navigator interface (conversation + canvas; mandate cards; receipt tiles; the audit chain IS the Finder), the steelmanned pass + counters, the flip condition (a risk owner accepts a Flint receipt as evidence), first customer (AP-ops firm), the $10M/$100M/$1B path, and the honest sequencing of DRM/licensing/ royalty into acts two and three. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
…honest attribution Sprint 1 keystone for the Flint 'accountable agent' wedge. handle_file_connection now captures the iroh-QUIC-verified remote node-id, encoded to the runtime's canonical did:key namespace (public_key_to_did, the inverse of did_to_public_key), and threads it to the provider-invoke gate. - Trust from a verified DID, not the transport (CARRIER.md): a peer is authenticated ONLY if its did:key is on ELASTOS_CARRIER_TRUSTED_PEERS (accepts did:key or raw node-id). Empty/unset (default) => no authenticated peers => every peer stays read-only: fail-closed, zero behavior change until an operator opts a peer in (P11). - Authenticated peers get a widened plane (content push-replication writes) AND a VERIFIED principal injected onto the fields the content coordinator ACTUALLY attributes quota+ownership on — publisher_did AND object_did (not just principal_id, which the provider never reads; effective_publisher_did honors a caller-supplied publisher_did, so overriding only principal_id would have left T1 open). So an allowlisted peer can only write content attributed to and owned by ITSELF; the anonymous plane stamps carrier:anonymous (P3 no ambient authority). - key/decrypt/drm/rights stay REFUSED even authenticated (P16 least privilege) — cross-node key-material flows need their own per-flow capability design (residual). Reviewed by the principles guardian (ALIGNED after fixes) and adversarially red-teamed: both independently caught that the first cut overrode the wrong field (principal_id) and would have left T1 open, plus the did:key/node-id encoding mismatch — both fixed here. Tests assert on publisher_did (a caller-supplied did:key:zVictim is proven overridden to the verified peer); the mock now echoes publisher_did so it can't mask the gap again. 6 new ratchets; clippy clean; 137 carrier tests pass. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
…ceipt' primitive Sprint 1, Item 2 for the Flint 'accountable agent' wedge. A MandateReceipt is an exportable, JSON-portable bundle of the signer's PUBLIC key + the signed, hash-chained records (the mandate + the actions taken under it), and verify_mandate_receipt() is a PURE, standalone verifier (no AuditLog, no disk, no runtime) — the 'an auditor/insurer can check it off-box' primitive that turns the tamper-evident chain into an admissible artifact. Verification mirrors verify_chain byte-for-byte (same compute_record_hash; ed25519 sig over the RECOMPUTED hash; T4 downgrade refusal — alg must be ed25519), plus contiguity (prev_hash linkage + seq+1). Principles-guardian + red-team both CONVERGED on two issues; both fixed here: - Pinning made FIRST-CLASS. The verdict splits structurally_valid (sound under the receipt's own key) from authenticated (sound AND signed by the caller-PINNED expected signer). authenticated is the bit an auditor acts on; without a pin an attacker's self-signed receipt is structurally valid but NEVER authenticated. verify_mandate_receipt now takes expected_signer_hex: Option<&str>; a ratchet proves a forged self-signed receipt fails against the pinned real signer. - Honesty (P12): docs state it is tamper-EVIDENT not tamper-proof; end-truncation needs an external head anchor (starts_at_genesis covers the front); verification needs a byte-compatible encoder. Plus schema fail-close + saturating_add overflow guard. 8 ratchets (standalone-over-the-wire, pinning-required, event tamper, sig forgery, dropped record, wrong key, wrong schema, memory-only-none); clippy clean. Documented residuals (follow-ups, same class as the chain's own caveats): a cryptographic end-truncation completeness anchor, and canonical event bytes for cross-language verification. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
…against holder tampering
Adds MandateReceiptScope so a MandateReceipt can prove one delegation's
activity, not just a contiguous chain slice:
- Scope::Capability { token_id } collects every record for one capability
token (grant + uses + revoke), which are interleaved (non-contiguous) in
the chain. export_mandate_receipt_for_capability(token_id) produces it.
- Verification for a Capability receipt requires: every record bound to the
token, exactly one CapabilityGrant present, and strictly ascending unique
seq (no duplicate/reorder).
Closes the completeness gap our principles-guardian and red-team both found:
a filtered set is otherwise a keyless filter any holder could trim (drop an
inconvenient use/revoke) while the receipt still verified. The issuing
runtime now signs the exact ordered record SET (set_binding, ed25519 over a
domain-separated scope+count+record-hash message); verify_mandate_receipt
re-checks it (set_binding_ok) so any add/drop/reorder by a holder in transit
is detected. Required for Capability scope; optional-but-verified for
Contiguous. This binds against holders, not the key-holding issuer — the
same tamper-evident-not-tamper-proof bound the chain's signing key carries,
and the docs now state that honestly rather than implying an attestation the
bundle does not contain. starts_at_genesis documented N/A for Capability
scope (the grant sits mid-chain).
Ratchets: dropped-use is caught by set_binding while scope_ok stays true
(the exact reviewer scenario), duplicated record rejected, foreign-token
record rejected, missing-grant rejected, clean receipt authenticates when
pinned. 13 receipt tests; full runtime lib suite green.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
…receipt verification
Adds the counterparty's side of the Flint "admissible receipt": an auditor,
insurer, or regulator is handed a receipt .json and runs ONE command to learn
whether it is authentic — with no runtime, no daemon, no network. Wraps the
pure verify_mandate_receipt and maps the verdict to fail-closed exit codes a
script can trust:
0 AUTHENTIC (--signer pinned AND matched AND structurally valid)
1 INVALID (a hash/signature/scope/set-binding check failed)
3 VALID-BUT-UNAUTHENTICATED (sound, but no pin / pin mismatch — NOT trust)
4 COULD-NOT-EVALUATE (bad file/JSON/--signer — kept distinct from 1 so
"couldn't read it" is never read as "it was forged")
--signer accepts either a did:key (the runtime's canonical principal
namespace) or the bare 64-char hex of the ed25519 key; both canonicalize to
the same pin. --json emits the full verdict for programmatic consumers.
Principles-guardian + red-team gate applied. Folded in: distinct exit 4 for
input errors; a present-but-empty --signer now errors rather than silently
degrading to "no pin"; and (red-team's one real break) every attacker-
controlled string in the human report — schema, signer key, token_id — is
sanitized so a crafted receipt cannot inject a fake "Verdict: AUTHENTIC" line
or ANSI cursor moves. Exit code and --json were already injection-safe. The
report also states honestly what an AUTHENTIC verdict does NOT prove
(capability: completeness only against holders, not a compromised issuer;
contiguous: end-truncation needs an external head anchor).
7 unit tests (exit-code mapping, did:key/hex pin equivalence, holder-trim
rejection, injection neutralization); process-level smoke confirms the codes.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
…evoke, receipt export Closes the Flint loop from the operator's side: grant -> act -> revoke -> prove, with no new authority surface. - `elastos mandate grant --capsule .. --resource .. --action .. --method .. [--ttl-secs N]`: mints a real signed capability token + standing envelope through the running runtime's shell-scoped /api/standing-grants/issue. The CLI holds no key and writes nothing itself: it attaches over the loopback control plane (attach-secret -> short-lived shell token), so the runtime stays the single writer of the audit chain. - `elastos mandate revoke <token>`: the substantive fix. The standing-grant revoke previously only flipped the in-memory envelope flag — the durable audit trail showed a revoked mandate as live forever. The handler now first revokes the BACKING TOKEN via CapabilityManager::revoke (emit-before- mutate: the signed CapabilityRevoke lands on the chain or the revoke aborts loudly, per the AUD-3 doctrine), then kills the envelope. - `elastos mandate receipt <token> [-o file]`: new shell-only GET /api/mandate/:token_id/receipt exports the portable per-capability MandateReceipt (404 when absent — absence reported, never fabricated); the CLI prints the matching verify-receipt command for off-box checking. - /api/standing-grants/issue now also returns token_id (== grant_id), surfaced explicitly because it keys the mandate's audit trail. Ratchet: a full-loop handler test (grant -> use -> revoke -> receipt) proves the exported receipt carries the revoke, is set-bound, and authenticates against the runtime's pinned signer; unknown mandate 404s, malformed id 400s. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
…ent, casing desync, honest CLI Review pass on the mandate lifecycle (guardian: PASS w/ advisories; red-team: two real breaks + latent gaps). All folded: - DURABLE ENFORCEMENT (red-team #1): production built its CapabilityStore in-memory, so individually-revoked (unexpired) tokens would validate again after a restart — a revoke durably ATTESTED but not durably ENFORCED. The server now builds the store with_persistence (revocations + epoch survive restart) and fails closed at boot if persistence is unavailable. - ID-CASING DESYNC (red-team #2): hex::decode accepts UPPERCASE but the standing-envelope registry is keyed by the token's lowercase Display form, so an uppercase id revoked the token yet left the envelope (the dispatch path's only check) live. The handler now kills the envelope by the canonicalized id. Regression test: revoke_with_uppercase_id_still_kills_the_standing_envelope. - HONEST CLI (guardian F2/F4): the revoke headline now states exactly what is attested; revoked:false becomes an unmissable stderr WARNING (a mistyped id attests a revoke for a token that never existed while the real mandate stays live). The receipt command no longer inlines the receipt's own embedded signer into the suggested verify command (pinning a document's self-carried key is circular); it prints it as informational and tells the operator to pin the key they trust out-of-band. - TRACKED LATENT GAPS (guardian F1/F3, red-team #3/#4): recorded as G-M1..3 in docs/KNOWN_GAPS.md — the intent-channel dispatcher (zero non-test call sites today) must consult token revocation + epoch when wired, and its acts must reach the per-mandate receipt; same-uid control-plane trust boundary documented. Lifecycle test comment now states honestly which act path it models. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
Sprint 4 "The Demo That Sells", part 1 — the data spine of the mandate card
and a scripted, nothing-simulated walkthrough:
- StandingGrantStore::list / StandingGrantService::list: enumerate every
standing envelope this runtime lifetime, REVOKED ONES INCLUDED (an
operator surface shows what was killed, it does not erase it), sorted for
a stable listing; a poisoned lock degrades to empty (absent), never a
fabricated grant.
- GET /api/standing-grants (shell-only): mandate cards {token_id, capsule,
resource, action, methods, expires_at, revoked, active} plus the runtime
audit chain's signer key offered over the AUTHENTICATED loopback control
plane — the operator's out-of-band pin for verify-receipt, as opposed to
the circular receipt-self-pin.
- elastos mandate list: renders the cards with honest LIVE/REVOKED/EXPIRED
states.
- elastos mandate demo: runs the whole loop ONCE against the live runtime —
real grant, real list, real revoke (durably attested), real receipt
export, and an in-process verify_mandate_receipt pinned to the signer
fetched over the authenticated channel. Nothing simulated; every record
lands on the real chain; the demo fails loudly if the receipt does not
authenticate.
Handler ratchet: mandate_list_shows_live_and_revoked_cards_with_signer_pin
(live card active, revoked card listed+flagged, pin matches the audit key).
Full server suite green (1114); clippy clean.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
…, demo cleanup, honest pin labeling Guardian (CHANGES-REQUIRED) + red-team (DO-NOT-SHIP) findings, all folded: - LIVE-after-mass-kill (red-team blocker): the epoch advance (revoke-all) killed every backing token but the standing-envelope registry knew nothing of it, so an epoch-dead mandate kept rendering LIVE. Fixed systemically: StandingGrantStore/Service::revoke_all() kills every envelope and the revoke-all handler calls it alongside the epoch advance; the mandate card additionally cross-checks the manager's individual token-revocation store (new read-only CapabilityManager::is_token_revoked probe), so a mandate whose token died by ANY route can never render live. Unparseable ids are fail-closed inactive. Ratchet extended: after revoke-all, no card may be active. - Stranded demo authority (red-team blocker): a demo failure after the grant left a REAL 1h mandate live. The post-grant steps now run in a fallible inner fn; on any error the demo attempts a cleanup revoke and, if even that fails, prints the exact revoke command — never a silently stranded authority. - Fabricated-clean list on a poisoned lock (guardian F1): list() now recovers via into_inner() exactly like the store's writes (single- statement invariant) instead of rendering an empty "no mandates" over a map that has entries. - Honest pin labeling (both reviewers): the demo's step 5 no longer prints "AUTHENTICATED/independently" — it reports "pin matched" with explicit provenance (the pin is self-asserted by this runtime over its control plane; the demo shows the pinning MECHANISM, a counterparty verifies on their own box with an out-of-band pin). ListMandatesOutput's doc states the trust root honestly (client-auth attach; runtime identity rests on the 0600 coords file + loopback, not a server credential). Gate: clippy clean; elastos-server 1114 + elastos-runtime 399 tests green. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
…(closes G-M1/G-M2) Sprint 5: the last unlit leg of the mandate loop. An authenticated agent intent now executes under its standing mandate, fail-closed, and the act lands in the mandate's receipt. POST /api/standing-grants/dispatch (shell-only) → dispatch_standing_intent: - authenticates the signed IntentDeclarationV1 (verify_self) — a forged declaration is rejected 400 before any grant lookup or record; - G-M1 CLOSED: consults the manager's individual token-revocation store (new read-only CapabilityManager::is_token_revoked) BEFORE the gate and self-heals the envelope to revoked, so a backing token killed by ANY path (not just standing-revoke) denies dispatch with the honest `revoked` reason. The epoch/revoke-all path was already closed in Sprint 4; - runs the fail-closed intent gate (declaration recorded on-chain BEFORE the act; no custody ⇒ no act; envelope binds capsule + method + resource + action, exact match), the act issuing the signed affordance receipt; - G-M2 CLOSED: records a token-keyed CapabilityUse (success mirrors the outcome) so export_mandate_receipt_for_capability carries every intent-channel act AND every denied attempt, not just validate-path uses. Ratchets: dispatch_acts_under_the_mandate_and_the_receipt_carries_the_act (receipt shows uses=[acted:true, denied:false] and still authenticates); dispatch_denies_when_the_backing_token_is_dead_by_any_path (manager-side kill while the envelope still believes it is live ⇒ denied/revoked + envelope self-healed; forged intent ⇒ 400). KNOWN_GAPS G-M1/G-M2 marked CLOSED. Gate: clippy clean; elastos-server 1116 tests green. Reviews (guardian + red-team) in flight; findings fold into a follow-up per the sprint standard. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
…ing, epoch liveness, honest "authorized" Both reviewers returned DO-NOT-SHIP on the act leg; all blockers fixed with ratchets. This leg executes agent authority, so the honesty bar is highest. - REPLAY / double-act (red-team F5, blocking): nothing deduped intent_id, so a captured/retried signed declaration acted N times — a double-spend for a value-moving mandate. Added a per-runtime replay guard (StandingGrantStore::record_fresh_intent); a re-POSTed intent is refused 409, no second act, no second receipt use. Ratchet: dispatch_refuses_a_replayed_intent. In-memory (per-lifetime) bound tracked as G-M5. - SIGNATURE BOUND TO NO AGENT (both reviewers, blocking): verify_self only proves "signed by the key it names" — any key. The envelope bound a capsule STRING, so anyone naming a grant_id acted under it with false attribution. Mandates can now bind an authorized agent ed25519 key (issue --agent-key / mandate grant --agent-key); when set, the gate requires the intent to be signed by THAT key (EnvelopeDenial::WrongAgent). Ratchet: dispatch_binds_the_authorized_agent_key. Unbound default (weaker, contained by shell-scope) tracked as G-M4. - EPOCH/ROTATION LIVENESS (guardian, blocking): is_token_revoked missed the epoch, so after a key-rotation epoch advance dispatch acted under epoch-dead mandates. The envelope now captures the token epoch at issue and the handler checks is_epoch_valid alongside revocation, healing the envelope. Ratchet: dispatch_denies_after_an_epoch_advance. - HONEST NAMING (both reviewers, P12): the act mints a signed receipt from the declared fields — no real executor — so reconciliation is structurally matched. Outcome renamed "acted" -> "authorized"; success now derives from the reconciliation STATUS (a real Matched delivery), not merely gate-passed; docs state it attests authorization + custody, not a side effect (G-M6). - HYGIENE (red-team F6): deny_unknown_fields on IntentDeclarationV1; is_overbroad_grant_resource guard at issue_standing_grant. - Residual TOCTOU (narrow probe/gate window) documented under G-M1. KNOWN_GAPS updated (G-M1/G-M2 closed with the epoch + best-effort caveats; G-M4/G-M5/G-M6 added). Gate: clippy clean on changed files; elastos-server 1119 + elastos-runtime 399 tests green. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
… now an independent observation (G-M6) Closes the last honesty gap: dispatch no longer mints the affordance receipt from the declared fields (which made reconciliation structurally always Matched). It invokes a real IntentExecutor and mints the receipt from what the executor REPORTS it performed, so the reconciliation reflects reality: - a method with no executor (or one that declines) -> Undelivered -> outcome "authorized_not_performed", receipt use success=false (not a fabricated match); - an executor that acts on a different field -> Diverged; - only a faithful performance -> Matched -> outcome "performed", success=true. Folded in from the guardian (CHANGES-REQUIRED) + red-team (SHIP the seam, DO-NOT-SHIP side-effecting executors without fixes): - P11 (blocking): the executor now runs INSIDE the gate's act closure, so it fires ONLY after authorization — a denied/revoked/wrong-agent intent never executes. Previously execute() ran before dispatch(), a landmine for any future side-effecting executor (effect before denial, inverting no-custody-no-act). - P12: the production executor set is DELIBERATELY EMPTY (MethodRegistryExecutor::production()). Shipping a no-op that echoed the declaration would re-introduce a durable success=true "write" for a method that performed no write. Every production method is now authorized_not_performed until a real affordance is wired; the performed/diverged plumbing is proven by test executors. - An executor that PERFORMS but reports an unrepresentable action now surfaces a 500 rather than silently under-reporting the act as "not performed" (hiding an effect is worse than surfacing it for an accountability system). - Corrected the stale handler doc; softened KNOWN_GAPS G-M6 to state the seam is independent for the Undelivered/Diverged legs and that Matched proves report-fidelity, not reality (trusted-core executor assumption). Ratchets: dispatch_reconciles_unperformed_and_diverged_acts_honestly (unregistered -> Undelivered; shifting executor -> Diverged; both success=false), plus the performed-path tests injecting a faithful test executor. Gate: clippy clean on changed files; elastos-server 1122 tests green. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
…makes "performed" literal Sprint 7 registers the first genuine executor behind mandate dispatch, so "performed" stops being a documented empty-registry placeholder and becomes a real, side-effect-free operation the runtime actually does. runtime.audit_verify re-verifies the runtime's own tamper-evident audit chain (hash links + ed25519 signatures) — a pure read — and PERFORMS iff it truly verifies, DECLINES (=> Undelivered) otherwise. The outcome tracks real chain state, not the declaration: a corrupt or unsigned chain is honestly authorized_not_performed. Proven end to end through the PRODUCTION executor (no test stand-in): a read mandate + agent-key-bound signed intent => outcome "performed", receipt CapabilityUse success=true, authenticates. Folded in from guardian (CHANGES-REQUIRED) + red-team (SHIP + one fix): - Report what was TRULY done, not the declaration (guardian #1, P12/P16): the executor now reports resource = elastos://runtime/audit-chain (the whole chain it really reads, NOT the declared resource), action = read, and input_hash = empty (no arguments). So a mandate mis-scoped to another resource reconciles Diverged, never a misleading Matched (audit_verify_under_a_misscoped_mandate_diverges). - Require a signing key (red-team #1, P12): verify_chain(None) is a hash-only walk over a public algorithm, so an unsigned/memory-only log now DECLINES rather than over-claim a signature-verified read. A "performed" provably means signature-verified. - Fixed the now-stale "production executor set is empty" claims in the dispatch doc and KNOWN_GAPS G-M6 heading. - Documented residuals: verify_chain is O(chain) per call (DoS-bounded by the shell-only + agent-key-bound + replay-guarded mandate); the verified count is observed but not surfaced; a torn concurrent log line yields a fail-closed spurious Decline. Gate: clippy clean on changed files; elastos-server 1125 tests green. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
…form → revoke → deny → prove) `elastos mandate demo` now drives the whole accountability loop with a REAL agent action, not just operator-side grant/list/revoke/receipt: 1. GRANT a read mandate for runtime.audit_verify, bound to a freshly generated agent key (agent_pubkey). 2. LIST — the mandate is live. 3. ACT — the agent signs a runtime.audit_verify intent with its own key and dispatches it; the runtime REALLY re-verifies its tamper-evident audit chain (Sprint 7, signature-verified) → "performed". 4. REVOKE — durably attested kill switch. 5. ACT AGAIN — the same agent is denied with reason=revoked (the kill switch has teeth). 6. RECEIPT — exports the mandate's trail and narrates the ACTUAL records. 7. VERIFY — against the runtime signer pin (with the honest same-channel caveat). Adheres to the runtime principles: the agent acts only under a scoped, agent-key-bound, revocable mandate (P3/P16); every step reports what really happened (P12); the runtime stays the single audit writer. A handler ratchet full_demo_sequence_grant_perform_revoke_deny_prove mirrors the exact sequence and asserts the 4-record trail [grant, use_ok, revoke, use_denied] + reason=revoked + authenticates. Folded in from guardian (PASS) + red-team (SHIP): - run-unique intent ids (suffixed with the run's token id) so re-running the demo against a persistent runtime doesn't collide with the per-lifetime replay guard; - a 2xx-grant-but-unreadable-token-id now warns loudly (a live TTL-bounded mandate may exist; check `mandate list`) instead of returning silently; - the receipt step narrates the ACTUAL record kinds (best-effort use records) rather than a hardcoded trail; - the post-revoke denial asserts reason=revoked (not an incidental denial); - the demo output now discloses in-band that it plays the agent with an in-memory key (production: the agent holds its own key). Gate: clippy clean on changed files; elastos-server 1126 tests green. (A live CLI run needs an operator runtime with the localhost-provider capsule, absent in this sandbox; verified via the handler ratchet mirroring the exact flow.) Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
…-dependent read Adds a second wired affordance whose outcome tracks REAL variable state, not just chain integrity: `runtime.content_seen` answers "did the mandate's own capsule successfully open content id X?" — so the SAME agent + method + declaration reconciles "performed" or "authorized_not_performed" depending on what the runtime actually recorded, proving the reconciliation is a genuine independent observation. Backed by AuditLog::principal_opened_content (sync, streams the durable log, early-returns). Handler + executor ratchets prove the state-dependence and principal isolation end to end. Folded in from guardian (CHANGES-REQUIRED) + red-team (DO-NOT-SHIP, conditional): - CROSS-PRINCIPAL ORACLE (red-team #5 / guardian #4, blocking): the first cut matched ANY principal's access, letting an agent learn whether anyone accessed an id. Now PRINCIPAL-SCOPED — matches only ContentOpen with the caller's own principal_id ("did I open it", not "did anyone"). ContentFetch is dropped (it carries no principal, so it can't be attributed). A ratchet proves a different capsule asking about the same id is Declined. - RECEIPT OVER-CLAIM (guardian #1, blocking): CapabilityUse carries resource + action but not the method, so a content_seen "read of X" was indistinguishable from a real content read. The mandate is now scoped to a content-access-CHECK resource (elastos://runtime/content-access/<id>), so the receipt honestly names a read of the CHECK, not the content bytes — mirroring Sprint 7's canonical-resource pattern. - WEAKER EVIDENTIARY BAR (guardian #2): the scan didn't verify signatures while audit_verify Declines unsigned logs for the same offline-rewrite threat. The executor now requires the log signed + verify_chain Ok before the scan, so a matched ContentOpen is signature-attested — the same bar as audit_verify. Gate: clippy clean on changed files; elastos-server 1128 + elastos-runtime 399 tests green. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
…e visible A self-contained, dependency-free HTML dashboard that renders the mandate loop for an operator/stakeholder, faithful to the real backend shapes: - Header carries the trust anchor: the runtime signer pin (the key a counterparty pins off-box), the Flint mark + tagline. - A summary stat row (mandates / live / revoked / expired) over a grid of mandate cards — each with a state severity-stripe + pill (Live / Revoked / Expired, from the real active/revoked fields), the acting capsule, scope (action on resource), authorized methods as chips, and the token id; all cryptographic data in monospace. - Clicking a card opens the receipt drawer: the record timeline (grant → performed/denied acts → revoke, colored by CapabilityUse.success) with a Set-bound badge. Honesty by construction (P12): it presents the runtime's signer as SELF-ASSERTED and points to `elastos verify-receipt --signer <key you trust>` for real off-box verification — NO false "authenticated"/"verified" badge, and it states that a "performed" act attests the runtime did what was declared, bounded by trust in the issuing key. All API-provided data (capsule, resource, methods, token id, hashes, reason) is HTML-escaped before insertion; there are NO external resource references (CSP-clean). Reads the live /api/standing-grants and /api/mandate/:id/receipt when served in-runtime; falls back to clearly-labeled "Preview data" otherwise. Theme-aware, responsive. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
… badges
Guardian review (CHANGES-REQUIRED) on the mandate dashboard, all folded:
- BLOCKER: a failed LIVE receipt fetch silently painted the sample receipt
under a real mandate's token id — inventing history. Now a live fetch
failure shows an explicit "Receipt unavailable" error; the sample receipt
is painted ONLY in the labeled preview branch (no runtime reachable), and
the drawer carries a "Preview receipt" marker in that case.
- The "Set-bound" badge is now conditional on the receipt actually carrying
set_binding (else "Not set-bound") instead of an unconditional green check.
- Removed the dead expiry() helper and aligned the sample expires_at to the
real SecureTimestamp {unix_secs, monotonic_seq} shape (no future landmine).
- Escaped e.seq before insertion; the verify command now shows an honest
placeholder <the issuer key you trust> rather than echoing the receipt's
own (truncated, non-runnable) signer — reinforcing "pin the key you trust
out-of-band, do not pin the receipt's own signer".
Still fully self-contained (no external refs, CSP-clean), all API data
escaped, theme-aware. JS syntax-checked.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
Ships the mandate dashboard as a real capsule so the Home shell lists it in the launcher (auto-titled "Mandates", default glyph) and opens it in a draggable window like any other app — the native "opens in the shell" contract, mirroring the existing `creator` app exactly. - capsules/mandates/capsule.json — role:app, type:data, entrypoint:index.html, NO capabilities/permissions/interfaces (a de-privileged html frame that can only call same-origin APIs the shell already gates). - capsules/mandates/index.html — the self-contained dashboard, now the SINGLE source (the orphan copy under crates/elastos-server/assets was removed; it was referenced by nothing). - Ratchet: mandates_ships_as_a_launchable_browser_app proves the capsule is discovered as shell-launchable and resolves as a data/html browser capsule that serves its html. Honest limitation (deferred to the next sprint): the dashboard's live fetches hit the API server's Bearer-gated /api/standing-grants + /api/mandate/:id/ receipt, not the gateway origin that serves /apps/mandates/, so in the shell it shows its labeled PREVIEW fallback. Wiring live data means sharing the epoch/revocation-aware mandate registry into the gateway (so the shell never shows a revoked mandate as "Live" — the Sprint-4 lesson), a serve-architecture change worth its own careful sprint. The preview banner now states this honestly: "not live mandates from this runtime … nothing here reflects real grants yet." Gate: clippy clean; elastos-server 1129 tests green; capsule JSON + dashboard JS syntax-checked; no external resource refs. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
…rrect stale comments Guardian/red-team review returned PASS; folding the two cheap honesty nits: - In PREVIEW mode the "Runtime signer" pin showed the fabricated sample key under an authoritative label with no marker. It now renders as "sample · <key>" so it can never be mistaken for this runtime's real signer (the page-level banner already flags preview; this marks the one residual authoritative-looking element). - Corrected two stale "no runtime reachable" code comments to describe the real reason for preview (the gateway data route isn't wired yet). Non-folded (noted follow-up): the launchable-app ratchet exercises the dev-root path only; a packaged install gated by components.json is untested — tracked for the live-data/serve sprint. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
…nt 12) The mandates ElastOS shell app now reads real mandate data from the runtime instead of representative preview data. A read-only sub-router on the home gateway serves the operator's mandate list and portable per-mandate receipts, gated by the same signed, app-bound home-launch token every shell app uses. - Hoist ONE shared StandingGrantService + CapabilityManager into ServerInfrastructure, thread the SAME handles to both the API server and (via the supervisor) the gateway, so the shell reads exactly the registry the API server issues mandates into — never a stale/separate copy. - New api/gateway_mandates.rs: MandateApiState + two GET routes (/api/apps/mandates/standing-grants, /api/apps/mandates/mandate/:id/receipt). Both require_home_launch_token(.., "mandates"); strictly read-only. Merged into the gateway only when both handles are present (control-plane = none). - The capsule sends the home_token (x-elastos-home-token) it is launched with; live only in-shell, labelled sample when opened standalone, honest "unavailable" banner on an in-shell fetch failure (never sample under live). Council fold-in (guardian + red-team, fable): - HIGH: the capsule never sent the launch token — every fetch would 401 and the "live" path was dead on arrival. Fixed: read home_token from the launch URL. - The card projection is now ONE shared helper (handlers::capability:: mandate_cards) used by both surfaces, and its `active` bit consults epoch validity (is_epoch_valid) in addition to the envelope flag and individual revocation — closing a pre-existing gap where a revoke_all / key-rotation epoch advance let both list surfaces render an epoch-killed mandate as "Live". Gate: clippy clean; elastos-server lib suite 1134 passed. New ratchets: gateway_mandates::tests (auth-required on both routes, live registry reflected, epoch-killed mandate marked dead, honest 404 on absence). Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
The Mandates shell app gains its first (and only) mutation: revoking a
mandate from the receipt drawer. Revoke exclusively REMOVES authority —
the fail-safe direction: the worst a stolen mandates launch token can do
on this surface is kill an agent's autonomy early.
- handlers/capability.rs: extract revoke_mandate — the ONE shared kill
path (canonicalize id → durably attest the signed CapabilityRevoke
BEFORE the envelope dies, per AUD-3; an unattestable revoke ABORTS
loudly) — now used by both the API server's revoke_standing_grant and
the gateway route, so the fail-closed order cannot drift.
- gateway_mandates.rs: POST /api/apps/mandates/standing-grants/revoke,
same app-bound home-launch-token gate as the reads (401 kills nothing),
deny_unknown_fields body, {revoked:bool} honest idempotency (true iff
a live envelope was killed by THIS call).
- capsules/mandates/index.html: two-step kill switch (arm → confirm) in
the receipt drawer, shown only in-shell on a LIVE mandate (also when
the receipt itself fails to load — an operator may need to revoke
exactly when things look wrong). Success repaints the list from the
SERVER; a transport failure is reported with honest uncertainty (the
runtime attests before replying — the card state is authoritative).
- gateway_server.rs + KNOWN_GAPS.md: the surface is no longer described
as read-only; the fail-safe-direction trust decision is documented.
Gate: clippy clean; elastos-server lib suite 1137 passed. New ratchets:
revoke_route_requires_home_launch_token_and_kills_nothing,
revoke_route_kills_the_mandate_and_is_idempotent,
revoke_route_rejects_malformed_id.
Council review (guardian/red-team/UI-honesty with adversarial verify) is
running; confirmed findings land as a follow-up commit this sprint.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
…itch race + prove-surface honesty Council review (guardian / red-team / UI-honesty, every finding attacked by 3 adversarial refuters): all three verdicts SHIP-WITH-FIXES. The Rust kill path survived untouched (3 findings refuted as pre-existing / fail-safe); all confirmed findings were in the capsule UI: - MEDIUM (upheld 3/3): a SLOW receipt fetch for mandate A landing after the operator opened mandate B repainted the drawer with A's receipt AND A's kill switch under B's header — arm+confirm would revoke the WRONG agent. Fixed: the drawer tracks its owning token id (OPEN_TID); every async continuation bails if it no longer owns the drawer, and the kill switch now prints its TARGET id beside the button (arm label carries it too). - LOW (upheld 3/3): after a successful revoke the drawer still showed the PRE-revoke timeline directly under "the receipt now carries it". Fixed: re-fetch and repaint the receipt on success so the visible evidence matches the claim; if the re-fetch fails, say the record is not shown. - LOW (upheld 2/3): the transport-failure note claimed "the list has been refreshed" past-tense before (and regardless of whether) the async refresh ran. Fixed: in-progress tense, defer to the reloaded card state. Ratchets re-run green (9 mandates tests incl. launchable-app); capsule JS syntax-verified. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
…start (close G-M5) The serve-path StandingGrantService is now snapshot-backed at data_dir/standing_grants.json (version-pinned, atomic temp+fsync+rename, mirroring CapabilityStore): a granted mandate is still LIVE after a reboot, a revoked one STAYS dead (never crash-revived), and a dispatched intent_id is still refused — the replay guard persists. Ordering is durable-before-visible with rollback on EVERY mutation (issue / revoke / revoke_all / record_fresh_intent): a snapshot write that fails is rolled back in memory and the error surfaces — - issue handler refuses to mint (500, "not issued"); - dispatch refuses the intent when the replay guard or the G-M1 heal cannot be durably recorded (true reason, never a fabricated "replay"); - revoke surfaces "token durably revoked, envelope record failed, retry"; - mass revoke reports failure rather than a clean success whose envelopes could resurrect as Live cards. Boot is fail-closed: a present-but-corrupt or wrong-version snapshot refuses to start (a skipped record could resurrect a revoked mandate or reopen a replay window); a missing file is a clean first boot. Same-disk custody caveat documented (mirrors the audit head-anchor). The carrier_bridge's separate registry stays memory-only by design. Ratchets: persistent_store_survives_reopen_live_dead_and_replay, persistent_store_refuses_corrupt_state_at_boot, persist_failure_rolls_back_and_surfaces (dir-squat write-failure seam), mandates_survive_restart_over_the_handlers. Gate: clippy clean, 402 runtime + 1138 server tests green. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
…lity + widen-proof reload + honest claims Guardian + red-team both returned SHIP-WITH-FIXES on the durable-mandates core (persistence design, rollbacks, lock order, boot behavior all verified sound). Confirmed findings folded in: - Power-loss replay window (red-team F1 / guardian F4): the snapshot rename is now DURABLE, not just atomic — fsync the parent directory after the rename (unix). Without it a power cut could revert to the old snapshot; for the replay guard that revert is a replay window (a captured signed intent acting twice), with no token-revocation backstop like a reverted revoke has. - Widen-proof reload (red-team F2 / guardian F3): deny_unknown_fields on the snapshot AND the envelope, plus presence-REQUIRED Option keys — a hand-repaired snapshot that drops agent_pubkey can no longer silently UNBIND an agent-bound mandate (serde's missing-Option=None), nor immortalize one by dropping expires_at; explicit null still loads. New ratchet: reload_refuses_dropped_option_keys_and_unknown_fields. - Honest claims (guardian F1/F2/F5): the revoke error now claims exactly what the manager guarantees (signed record durable + revoked in this runtime — token-state persist is best-effort); the mass-kill error no longer asserts "all tokens dead" (advance_epoch can fail silently); the replay 409 says "consumed", not "dispatched" (a refused intent burns its id without acting). - Loud fallback (guardian F6): a ServerConfig without the shared registry now WARNS that the memory-only fallback loses G-M5 durability, instead of degrading silently. KNOWN_GAPS G-M5 updated to claim exactly these bounds. Gate: clippy clean, 403 runtime + 1138 server tests green. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
Connects Flint's pay affordance to the Elacity on-chain DRM
marketplace: a DrmMarketplaceProvider behind the SAME PaymentProvider
trait the HTTP rail implements, so the meter, ledger, two-generals
classification, and portable receipt are byte-identical whichever rail
is wired (one pay spine, never a fork).
The provider: resolve the payee (a DRM asset KID/content-id) to its
unique (operative, tokenId) binding through the MKT-1-hardened
resolver — FAIL-CLOSED on ambiguity (an ambiguous or unresolvable
asset is NotCharged/refunded, never a fallback buy) — then settle via
buy_authority and classify two-generals: confirmed = charged;
provably-not-broadcast = NotCharged (refund); broadcast-then-
unconfirmed = Indeterminate (reservation kept, reconciled by the
intent-signature idempotency key). The chain boundary is behind two
injected traits (DrmResolver/DrmSettler): CI exercises every branch
with mocks; the live Base path (ChainDrmMarketplace) is an operator
runbook step, never a CI call. Selected by ELASTOS_PAYMENT_RAIL=drm
(requires the durable meter+ledger).
rail_ref on the receipt: the settlement's on-chain truth (tx hash +
operative:tokenId) is carried on the signed CapabilityUse.rail_ref and
thus the portable receipt, so verify-receipt shows WHICH tx settled
the payment. Back-compat is load-bearing and identical to S32's
responsible_entity: appended LAST with skip_serializing_if, so a
pre-S34 CapabilityUse re-serializes byte-identically and every pre-S34
signed chain still verifies. Threaded IntentExecution::Performed →
the dispatch pipeline → capability_use_with_rail_ref, bound only on a
Matched performance.
Ratchets: drm_marketplace unit tests (confirmed/ambiguous/
unresolvable/not-broadcast/indeterminate + conservative pre-broadcast
classification); e2e agent_buys_a_drm_asset_..._receipt_carries_the_
rail_ref, an_ambiguous_drm_asset_is_refused_refunded_and_never_
settles, an_over_cap_drm_buy_is_refused_before_any_chain_call,
an_indeterminate_drm_buy_keeps_the_reservation_and_files_a_pending_
entry; back-compat audit::tests::{rail_ref_is_present_when_set_and_
omitted_for_back_compat, a_signed_chain_mixing_pre_and_post_s34_uses_
verifies}.
Docs: FLINT_MANDATE_ENGINE (the wedge is real), new
DRM_MARKETPLACE_RAIL.md operator runbook, KNOWN_GAPS MKT-DRM (three
stated residuals: meter-unit⇄price reconciliation, tx re-verification
depth, royalty verification).
Gate green: elastos-runtime 432, elastos-server 1220; clippy clean
(pre-existing wasm.rs warning only).
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
…esty
Folds the two-seat council review of Sprint 34 (the Flint↔DRM wedge).
Both seats returned SHIP-WITH-FIXES; the consensus ship-blocker was the
refund classifier.
F1/F2 (HIGH, both seats — the money invariant): ChainDrmMarketplace's
is_pre_broadcast_refusal decided REFUNDS by loose substring-matching an
opaque, chain-controlled error string. The generic tokens "ambiguous",
"unresolved", "missing channel" cannot occur on the pinned settle path
— they can only appear inside a POST-broadcast RPC error
("chain-provider op failed: {rpc}"), so a coincidental match would
refund a tx that may have landed (refund + purchase both stand). Now
matches ONLY buy_access's exact, anchored pre-broadcast sentinels
(each carries the "(fail closed)" / "— fail closed" marker the RPC
format never reproduces, each fires strictly before broadcast);
everything else — every post-send error — stays Indeterminate (keep
the reservation). Ratchet pre_broadcast_refusal_classification_is_
conservative now asserts a post-broadcast "unresolved"/"ambiguous"
message does NOT refund.
Guardian F1 (HIGH honesty): buy_access returns Ok at broadcast
ACCEPTANCE, not inclusion — zero confirmations. Every "tx confirmed =
money moved" surface reworded to "broadcast-accepted on rail trust,
zero confirmations" (module doc, DrmSettlement doc, DRM_MARKETPLACE_
RAIL step 3 + honest bound 2, KNOWN_GAPS MKT-DRM residual 2, FLINT
doc). A Performed-but-unconfirmed record has no reconcile lever —
stated.
Guardian F3 (MED): the resolver's ambiguity classification was dead —
run_chain_capsule drops the error CODE and the message says
"binds >1 distinct", not "ambiguous", so a hostile MKT-1 co-mint
mislabeled as absence. Now classifies Ambiguous on the distinctive
message substring (+ the false comment corrected). Money-safe either
way; this keeps the signed refusal reason honest.
Guardian F4 (MED): build_pay_rail's DRM branch never checked
rights_mode — a dev-modes Dev build fabricates synthetic tx with no
ALLOW_MOCK opt-in. Now a non-Chain rights mode wires the DRM rail only
with explicit ELASTOS_ALLOW_MOCK_PAYMENTS, else UNWIRED (the S29 mock-
money discipline). Ratchet drm_rail_obeys_the_mock_money_discipline.
Guardian F5 (LOW): empty buyer principal / ledger now warn loudly at
boot (fail-closed at act time already). Guardian F6 (LOW): rail_ref is
re-sanitized at the audit emit boundary (defense in depth for any
future executor). Red-team F3 (LOW): rail_ref strips the ;= delimiters
from chain-supplied components (no forged binding) — ratchet added.
Red-team F4 (LOW): resolver now guards an empty operative, not just an
empty tokenId. Red-team F2 (MED, documented): the intent-signature
idempotency key is not plumbed to the chain — within-window double-buy
is closed by the replay guard, cross-window is a stated MKT-DRM
residual.
Also fixed a pre-existing dev-modes-only test break (buy_authority.rs
called buy_access with the old 4-arg signature) so the dev-modes lane
compiles and the F4 negative path runs.
Gate green: elastos-runtime 432, elastos-server 1223 (default);
drm_rail_obeys passes under --features dev-modes; clippy clean.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
Closes the two sharpest MKT-DRM residuals from S34: a DRM buy was
recorded Performed at broadcast acceptance (zero confirmations), and a
cross-window replay of a signed intent could double-buy.
Broadcast ⇒ Pending, confirm ⇒ Performed. DrmMarketplaceProvider::pay
now maps a broadcast-accepted buy to PayError::Indeterminate carrying
the rail_ref (tx), so the pay closure files a PENDING ledger entry with
the reservation HELD — a DRM buy is never charged at broadcast. A new
reconcile_drm_confirmations driver polls each pending DRM tx via
chain_tx::tx_confirmation_live (eth_getTransactionReceipt + a
confirmation-depth floor, ELASTOS_DRM_MIN_CONFIRMATIONS default 3):
mined+successful+deep ⇒ promote to ResolvedCharged AND bind the
rail_ref onto a token-keyed signed CapabilityUse (the receipt now
reflects the CONFIRMED tx); reverted ⇒ refund exactly once; not-yet-
mined/unreadable ⇒ hold Pending (never auto-charge, fail-safe). Reuses
the S30 reconcile_payment_core spine — the driver only supplies the
chain's verdict and the receipt binding. Injectable DrmConfirmer trait:
CI mocks, live Base is the runbook.
On-rail idempotency (closes cross-window double-buy for every rail).
The durable ledger is the dedup: the runtime.pay closure refuses to
re-charge a flint-<sig> key that already carries a
Performed/Pending/ResolvedCharged entry, so a re-dispatched identical
signed intent past the replay window resolves to the SAME buy, never a
second settle.
New back-compat fields (both skip_serializing_if, appended last):
PaymentRecord.token_id (binds a pending payment to its mandate so
confirmation can key the receipt) and the S34 CapabilityUse.rail_ref.
Ratchets: drm_marketplace::reconcile_drm_confirmations_promotes_
refunds_and_holds (+ parse_drm_tx, broadcast⇒Indeterminate);
capability::{a_drm_buy_is_pending_until_confirmed_then_the_receipt_
carries_the_rail_ref, a_redispatched_drm_buy_is_idempotent_and_never_
settles_twice}. Fixed a pre-existing dev-modes test break earlier;
existing S34 e2e updated to the two-phase model.
Gate green: elastos-runtime 432, elastos-server 1226; clippy clean
(pre-existing wasm.rs warning only).
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
Folds the two-seat council review of Sprint 35. Both seats returned
SHIP-WITH-FIXES; the central money invariant (no double-refund,
no refund-a-confirmed) already held. Two HIGH structural findings
converged on the idempotency guard's durability.
Red-team F1 (HIGH) + guardian F3 (HIGH) — the guard was blind if the
pending record failed to land AFTER an irreversible broadcast, and
money-bearing keys were evictable, both reopening the cross-window
double-buy. Fixed structurally:
- RECORD-BEFORE-BROADCAST: the pay closure now durably custodies the
key as Pending via PaymentLedger::begin_attempt BEFORE calling
provider.pay; if the ledger cannot custody it (per-capsule pending
cap, ledger full of money-bearing keys, persist failure) it REFUNDS
and DECLINES without broadcasting — money never moves into an
unrecordable state. After pay, finalize() transitions the placeholder
to the outcome. begin_attempt reopens a provably-nothing-moved
terminal for a legit retry and refuses a money-bearing key
(AlreadyActive).
- NEVER EVICT MONEY-BEARING KEYS: eviction now sheds only
NotCharged/ResolvedNotCharged (provably nothing moved); Pending/
Performed/ResolvedCharged are protected, so an eviction can never
forget a charged key. A cap full of money-bearing keys refuses new
inserts fail-closed.
Guardian F1/F2 + red-team F3 (HIGH/MED) — confirmation fail-closed:
tx_confirmation_live now gates BOTH verdicts on the depth floor (a
shallow revert is HELD, not refunded — a reorg could re-include it),
and an absent/empty/unparseable receipt status reads as HOLD not
success (a hostile receipt with a blockNumber but no status cannot
promote a buy). Extracted a pure classify_receipt() so these
money-critical rules are CI-testable without a live chain.
Wording/doc folds: guardian F3 (corrected the mis-stated "charged keys
never evicted" — now true by construction), F4 (added the ledger
snapshot back-compat test), F5 (FLINT doc no longer calls a DRM buy a
rail-trust attestation — it is chain-confirmed), F6 (reconcile doc
comment); red-team F2 (pending-quota availability regression),
F4 (best-effort receipt binding), F5 (drm:tx= discriminator) all stated
as tracked MKT-DRM residuals.
New ratchets: payment_ledger::tests::{money_bearing_keys_are_never_
evicted, begin_attempt_custodies_reopens_and_refuses, pre_s35_ledger_
snapshot_without_token_id_round_trips}; chain_tx::confirmation_tests::
classify_receipt_is_fail_closed_on_status_and_depth. Serialized the two
env-mutating build_pay_rail tests behind a shared lock.
Gate green: elastos-runtime 432, elastos-server 1230; clippy clean.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
Closes the primary open MKT-DRM residual: the cap was enforced in spend
units while the actual on-chain charge is the listing's pay-token
amount, so the meter bounded intent, not the money leaving the wallet.
The price gate. The DrmSettler trait gains quote() — a READ-ONLY price
source (buy_authority::quote_buy, no broadcast). The provider now:
resolve → quote → PRICE-GATE → settle. The gate converts the mandate's
cap to pay-token units via a declared mapping (amount ×
ELASTOS_DRM_SPEND_UNIT) and refuses BEFORE broadcast if it does not
cover the on-chain price (NotCharged/refund; fail-closed on an
unparseable price or a conversion overflow) — never buy above what the
mandate authorized. settle() binds the gated price as the buy's
expected price, so buy_access's own abort-on-drift fires if the live
price changed between quote and broadcast (closes the TOCTOU: the buy
can never settle above the gated price).
Declared unit mapping, fail-closed. ELASTOS_DRM_SPEND_UNIT is
REQUIRED on the live Chain rail (pay-token smallest-units per spend
unit, e.g. 1000000 for USDC) — build_pay_rail refuses to wire the DRM
rail without it rather than silently assume 1:1. Dev/chain-mock quote
free, so the gate is a no-op there.
Receipt honesty. rail_ref now carries ;price=<price>;tok=<pay_token>,
so verify-receipt shows the pay-token price actually authorized, not
just the spend-unit amount.
Ratchets: drm_marketplace::tests::{a_buy_below_the_on_chain_price_is_
refused_before_broadcast (settler never called), an_exact_match_buy_
proceeds_and_the_rail_ref_names_the_price (unit scaling + rail_ref),
an_unparseable_price_is_refused_before_broadcast};
server::tests::drm_rail_obeys_the_mock_money_discipline extended for
the missing-unit wire refusal. The S34/S35 e2e updated for the new
quote step + rail_ref format.
Gate green: elastos-runtime 432, elastos-server 1233; dev-modes lane
passes; clippy clean.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
Folds the two-seat council review of Sprint 36. Both seats returned
SHIP-WITH-FIXES; the money-invariant findings were the ship-blockers.
F1 (both seats — the buy above the cap): the gate compared the mandate
cap against the listing's PER-UNIT price, but buy_access charges
price × quantity, and the DRM BuyTarget left quantity unset — so an
ambient ELASTOS_DDRM_BUY_QUANTITY>1 env could settle N× the gated
price. Fixed: the DRM buy PINS quantity=1 (a pay-for-access buy is one
ACCESS_TOKEN; the per-unit gate is now the total charge, and no env can
inflate it).
F2 (both seats — pay-token flip): the buy bound only expected_price, so
ensure_no_drift's payToken check was a tautology (compared the settle-
time re-read against itself). A seller swapping the listing's pay-token
at an equal numeric price between quote and broadcast would settle in a
different, possibly far-more-valuable token. Fixed: quote_buy now
returns the RAW pay-token (not a "native" alias), and the DRM buy binds
expected_pay_token = quote.pay_token, so abort-on-drift fails closed on
a token flip too.
F3 (guardian — one unit maps one token): a single global spend_unit was
applied to whatever token a listing quoted (heterogeneous per-listing
tokens make one mapping unsatisfiable). Added a REQUIRED (live Chain)
ELASTOS_DRM_PAY_TOKEN: a listing quoting any other token is refused
before broadcast, so the declared unit always denominates the declared
token and the ceiling stays literal. build_pay_rail refuses to wire
the live rail without it.
Extracted ChainDrmMarketplace::buy_target so the money-critical target
invariants (quantity=1, price + pay-token drift armed, pinned
operative/tokenId) are CI-testable without a live chain.
New ratchets: drm_marketplace::tests::{the_drm_buy_target_pins_
quantity_and_arms_price_and_pay_token_drift, a_listing_in_a_different_
pay_token_than_declared_is_refused}; server::tests::drm_rail_obeys_the_
mock_money_discipline extended for the ELASTOS_DRM_PAY_TOKEN wire
refusal. Docs (FLINT row, rail runbook, KNOWN_GAPS) qualify the
"literal ceiling" claim with the operator-declared-mapping residual and
document the quantity pin + pay-token requirement.
Gate green: elastos-runtime 432, elastos-server 1235; dev-modes lane
passes; clippy clean.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
…lity Four-seat quality council (money spine, dispatch surface, receipt primitives, docs) over the whole Flint core. Every small-and-safe finding folded; structural refactors ledgered in docs/CODE_QUALITY_LEDGER.md. Money spine: - Pre-broadcast refusal sentinels are shared consts (buy_authority) now referenced by both the producers and the DRM refund classifier, so a rewording can never silently degrade a provable refund into a hold; pinned by a sentinel-anchor test. - Ledger admission (per-capsule pending cap + money-bearing-aware eviction) extracted to one admit_new_key home used by record and begin_attempt; a failed reopen now restores the WHOLE prior record (status, rail_note, refund_applied) with a ratchet test. - record/record_with_token re-documented as test-seeding only (the production path is begin_attempt/finalize). - ensure_budget post-publish persist failure now reports Poisoned (the honest variant), matching the file's own S29 F1 rule; ratchet added. - reconcile_drm_confirmations Confirmed/Reverted arms share one spine; quote/settle sold-out refusal is one const; parse_drm_tx doc + dead unwrap_or cleaned; contradictory test name fixed; RAIL_TIMEOUT named; in-flight slot guard simplified; guarded unwrap in tx_confirmation_live restructured to a match; buy_authority tests moved to tempfile. Receipt primitives: - The frozen-serialization invariant now lives on AuditEvent and ChainedRecord themselves (not just three fields): field order/names on EVERY variant are load-bearing for every signed chain. - One shared recompute_record_hash canonicalization recipe used by both verify_chain and verify_mandate_receipt. - signable_digest never silently hashes empty bytes on a serialization failure (expect over unwrap_or_default) and the SecureTimestamp JSON preimage coupling is documented + byte-pinned by a ratchet test. - responsible-entity syntactic bound has one home (capability::responsible_entity_syntax_ok) used by all three choke points; denial-reason test now exhaustive (WrongAgent/NoGrant pinned); silent pubkey-publish and corrupt-counter-file failures now log loudly; dead allow(unused_imports) removed. Dispatch surface: - drm_rail test now takes the crate-wide ddrm_env_lock (flaky-CI race with rights/mint authority tests closed) with documented lock order; build_pay_rail test guards ELASTOS_PAYMENT_RAIL; one shared EnvGuard. - RequestCapabilityOutput docs corrected to the actual wire statuses; trivial drm_state wrapper removed; ELASTOS_WEB_MAX_SPEND_CAP now warns on an unparseable override; chain_tx test module moved past the items that followed it. Docs / presentability: - Flint is now discoverable from the front door: README Documentation table + What Works Today, and docs/README.md deep reference. - FLINT_MANDATE_ENGINE.md: branch-anchored opener replaced with what Flint IS; the 1,900-word runtime.pay table cell exploded into a real "payment spine" section; mandate=standing-grant vocabulary stated; stale test counts and sprint-log tone removed. - DRM_MARKETPLACE_RAIL.md: DDRM_LEDGER annotated as not-a-typo, REQUIRED-in-practice vars marked, and a three-surface payment-state mapping table (rail/ledger/dispatch) added. - KNOWN_GAPS.md retitled runtime-wide; MKT-DRM residual list headed honestly; GLOSSARY gains the Flint vocabulary; CODE_QUALITY_LEDGER.md tracks the deferred structural cleanups (CQ-1..CQ-10). Gate: runtime 434 + server 1237 + common 103 green; dev-modes lane green; clippy --tests clean in the Flint core. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
Closes KNOWN_GAPS MKT-DRM residual 2b: reconcile_drm_confirmations was correct but inert (operator/automation had to invoke it). The runtime now drives it unattended. The scheduler (spawn_drm_reconcile_scheduler, one per process, spawned next to the one rail build): - OFF BY DEFAULT: arms only when ELASTOS_DRM_RECONCILE_INTERVAL_SECS is set (u64 >= 1) AND the rail carries a DrmConfirmer (a new PayRail field, present only on the DRM rail — structurally un-armable on HTTP/mock). Malformed interval/batch env REFUSES to arm; an interval on a non-DRM rail warns and stays off. The arming decision is a pure fn (drm_reconcile_schedule_from_env), CI-pinned. - ZERO new money-moving code: a tick is the SAME reconcile_drm_confirmations pass the manual reconcile drives; every charge/refund still flows through reconcile_payment_core, exactly-once by the ledger's resolve semantics. - BOUNDED + ROTATING (council F1 fold): at most ELASTOS_DRM_RECONCILE_BATCH (default 64) entries per tick, overflow counted as `skipped`, and a rotating cursor starts each tick after the previous tick's last scanned seq (wrapping) — a stuck-unconfirmed prefix can never starve the entries (or refunds) behind it. - PANIC-ISOLATED per entry, with ledger-truth accounting (council F3 fold): a panicking confirmer/reconcile holds THAT entry and the tick continues; a panic after the durable resolution is accounted by what the ledger says and raises a money-state alarm, never a silent hold. - NON-WEDGING (council guardian F2 fold): at most one tick in flight — a hung chain RPC is skipped loudly each interval, never awaited into a dead schedule and never stacked into new blocked threads. Ticks run on the blocking pool; missed ticks delay (no catch-up bursts). - ATTESTED honestly (council F2 fold): a tick that SETTLED anything appends a best-effort signed drm_reconcile_tick event; idle and held-only ticks stay off the chain, so a stuck pending cannot grow the signed chain by one event per tick. Ratchets: a_tick_is_bounded_oldest_first_and_reports_what_it_skipped, a_stuck_oldest_entry_cannot_starve_the_entries_behind_it, a_panicking_confirmer_holds_that_entry_and_the_tick_continues, only_a_settling_tick_is_attested_held_and_idle_ticks_are_silent, the_drm_scheduler_arms_only_with_an_interval_and_a_drm_rail. Docs: DRM_MARKETPLACE_RAIL.md (env block + scheduler section + honest residuals), KNOWN_GAPS.md (MKT-DRM 2b CLOSED with stated residuals: opt-in by design; no subprocess read deadline yet; best-effort tick event), FLINT_MANDATE_ENGINE.md (payment-spine bullet). Gate: runtime 434 + server 1242 + common 103 green; dev-modes lane green; clippy clean. Council: two seats (principles guardian, red team), both SHIP-WITH-FIXES, all findings folded above with ratchets. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
…tor watches
The first slice of the North-Star surface, shipped inside the existing
Mandates shell app. STRICTLY read-only: every pixel is a projection of
the one enforcing registry + meter + ledger (same Arcs as the pay gate);
the panel has no buy verb — operators grant, agents act through the
existing signed-intent dispatch.
GET /api/apps/mandates/marketplace (launch-token gated, 503 when pay is
unwired — the Money panel's posture):
- ASSETS: every DRM asset the ACTIVE pay-mandates scope (resource
elastos://runtime/pay/<asset>, method runtime.pay), with live on-chain
quotes via the existing read-only quote_buy path. Quote reads are
SINGLE-FLIGHT (council fold: a miss claims an in-flight sentinel under
the cache lock before any read starts, so "one live chain read per
asset per TTL window" is literal under any concurrency — the S33 XSS
actor cannot amplify browser fetches into a blocking-pool storm),
TTL-cached (30s, pruned on every pass, post-fetch stamped), and
fresh-read capped per view with cache hits FREE — coverage rotates
across refreshes instead of permanently starving the tail (council
fold, with a rotation ratchet).
- BUYS: the ledger's records with honest wording — a broadcast is
"awaiting chain confirmation", never a purchase (regression-tested
against "purchas*"); confirmed carries its tx. Every PENDING buy is
ALWAYS shown (council fold: a flood of newer settled entries can never
push a live obligation out of sight — ratcheted); the settled tail is
windowed with the window STATED (buys_total/buys_limit, "showing X of
N" in the panel). Outside the live Chain rights mode the panel says
quotes are free/synthetic rather than displaying fake prices.
- UI renders exclusively via textContent (asset ids, quote errors, rail
notes and tx are external bytes; tx display server-bounded).
The demo lane: `elastos mandate market-demo <asset> [--amount N]` — cap
→ single-asset pay-mandate bound to an ephemeral agent key → the agent's
signed buy through the existing dispatch → revoke + cap cleanup (a
grant whose token id cannot be read warns LOUDLY about the live
authority, mirroring `demo` — council fold). The buy's ledger record
remains for the panel to show.
New read-only accessors: PaymentLedger::{recent(limit), len, is_empty}.
Council: two seats (principles guardian, red team), both
SHIP-WITH-FIXES; no XSS / auth bypass / money movement found; all
findings folded above with ratchets
(quote_coverage_rotates_instead_of_starving_the_tail,
a_pending_buy_is_never_truncated_by_a_flood_of_settled_entries, +
marketplace_projects_pay_mandates_and_ledger_buys_with_honest_wording,
marketplace_route_requires_home_launch_token,
marketplace_without_a_rail_reads_unwired).
Docs: FLINT_MANDATE_ENGINE.md (Watch row + operator-surface section),
DRM_MARKETPLACE_RAIL.md (Watching it — the panel + the demo).
Gate: runtime 434 + server 1247 + common 103 green; dev-modes lane
green; clippy clean.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
The loop's last structural gap closes: an agent could buy under a
mandate but not ask what things cost. runtime.market_quote is a READ
affordance behind the existing dispatch gate — quote the live terms of
a granted asset, and receive them in the dispatch response.
One quote spine (P5): a new crate::market_quote module owns the
single-flight, TTL-cached, read-only quote path (MarketQuoter trait;
LiveMarketQuoter = the existing quote_buy, no new chain code). The S38
Marketplace panel was REWIRED onto it and PayRail gained a shared
quote_cache Arc, so the panel and the affordance ride one identical
fan-out bound: at most one live chain read per asset per 30s window,
whoever asks, under any concurrency.
The affordance:
- MANDATE-SCOPED, no market-wide oracle: the resource is the same pay
resource the buy uses; the envelope gate's exact match confines
quoting to granted assets. One envelope carries ONE action, so quote
(read) and buy (execute) are TWO grants on one resource — the docs
and the demo now say and do exactly that (council guardian F1: the
demo originally issued one execute grant, so its quote was denied
wrong_action — dead on arrival; fixed with two grants, both revoked
at exit).
- EXPLICIT disclosure channel: IntentExecution::Performed gained
agent_visible_report — per-executor opt-in data surfaced as
DispatchIntentOutput.report ONLY on a Matched performance, bounded
and printable-filtered at the pipeline boundary (guardian F4,
mirroring rail_ref's emit-site discipline). market_quote discloses
the canonical terms; every other affordance passes None — state_get's
one-bit rule stays structural, pinned by an e2e negative control.
- HONEST reconciliation: discovery ("" declared) performs as declared;
attested mode echoes the ACTUAL terms so a changed listing reconciles
diverged, never a fabricated match — attesting the terms as of the
spine's last read (TTL caveat documented, guardian F3). A failed read
declines with the bounded error; dev/mock synthetic quotes are
documented as such (guardian F5).
- BOUNDED like money (council red-team F1): MAX_INFLIGHT_QUOTES=8 with
an RAII slot guard — K distinct assets against a hung RPC can no
longer park K blocking threads and starve the pay pipeline's pool;
over the bound refuses with retry, never queues. Chain-sourced terms
are held to the SAME bound as the agent's declaration before being
signed or disclosed (red-team F2 — validate what we sign); and the
cache prunes once per pass instead of per asset (red-team F3).
market-demo is now quote-THEN-buy: the agent reads the terms under its
read-mandate, decides, and only then dispatches the buy under its
execute-mandate — the first decision-making agent act in Flint.
Ratchets: market_quote_discovery_returns_terms_via_the_disclosure_channel,
market_quote_attested_mode_echoes_actual_terms,
market_quote_declines_on_read_failure_and_outside_the_pay_namespace,
market_quote_shares_the_single_flight_cache,
market_quote_refuses_malformed_terms_from_the_quote_source,
market_quote_inflight_reads_are_bounded_fail_closed,
an_agent_quotes_its_asset_and_state_get_still_discloses_nothing,
+ market_quote module unit tests.
Docs: FLINT_MANDATE_ENGINE.md affordance row (two-grants rule, TTL
caveat, dev-mode honesty), DRM_MARKETPLACE_RAIL.md (agent quoting +
demo).
Gate: runtime 434 + server 1256 + common 103 green; dev-modes lane
green; clippy clean. Council: two seats, both SHIP-WITH-FIXES, all
findings folded with ratchets.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
… chain
Closes the S37/S39-ledgered hung-read residual at its root. run_chain_capsule
(the ONE shared chain-provider subprocess helper — rights gate, resolve,
quote, buy prepare/broadcast, receipt/confirmation reads) now bounds the
whole conversation: ELASTOS_CHAIN_READ_DEADLINE_SECS (default 30; malformed
or <1 => the default, loudly), child spawned in its own process group, a
watchdog SIGKILLs the group at expiry, the child is always reaped.
The money line (both council seats verified SAFE at every consumer): a
deadline error carries CHAIN_DEADLINE_MARKER + "UNRESOLVED". On the SEND leg
it classifies INDETERMINATE (the tx may have broadcast — hold, reconcile),
NEVER a refund. On read legs it is an ordinary fail-closed refusal/hold.
Council: two seats, both SHIP-WITH-FIXES; all findings folded:
- pid/pgid-reuse kill window (red-team F1 / guardian F3): the watchdog is
disarmed and joined BEFORE the child is reaped, so it can only ever kill a
still-live, un-reaped child.
- unbounded response-line length (red-team F2): read_capsule_line is now
length-capped (MAX_CAPSULE_LINE = 4 MiB), so a firehose provider is a
bounded error, not an OOM.
- hostile-provider refund break (red-team F3, HIGH, pre-existing): a
`chain-provider op failed:` error is post-broadcast by construction, so
is_pre_broadcast_refusal now refuses to read any op-failed error as a
pre-broadcast refusal (=> Indeterminate/hold).
- error honesty (guardian F2): the deadline error keeps the underlying error
and `fired` is set only inside the unix kill.
- doc over-claim (guardian F1): the deadline bounds ONE chain conversation;
the wallet-provider sign leg and rights-provider decide leg are separate
undeadlined subprocesses — the tracked sibling residual. Plus the
availability-cliff note and comment-precision nits (F4/F5).
Ratchets: rights_authority::{a_hung_chain_provider_is_killed_at_the_deadline,
a_malformed_deadline_env_keeps_the_default_protection},
drm_marketplace::{a_chain_deadline_error_is_never_classified_as_a_refund,
a_post_broadcast_op_error_is_never_read_as_a_pre_broadcast_refusal}.
Gate: runtime 434 + server 1260 + common 103 green; dev-modes green; clippy
clean.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
…unded reap
Factor Sprint 40's chain-conversation watchdog into ONE shared module
(api/capsule_watchdog.rs) and extend the deadline discipline to the two
remaining unbounded subprocess conversations a money/access path traverses:
- wallet-provider SIGN leg (WalletSession::exchange): a timeout is a
PRE-broadcast refusal — the tx was never signed, so provably NotCharged.
drm_marketplace::is_pre_broadcast_refusal recognizes the marker ⇒ refund,
the exact mirror of the chain send-leg (Indeterminate/hold) rule.
- rights-provider DECIDE leg (run_rights_capsule): a timeout DENIES access
(fail-closed — every access consumer denies on Err).
The shared module owns the subtle bits so no third copy can drift: the
process-group spawn, the disarm-before-reap ordering (a recycled pid can
never take a stray group kill), the length-capped read, and the shared
deadline knob.
Council fold (principles-guardian + red-team, both SHIP-WITH-FIXES):
- guardian F1 (HIGH) + F2: the disarm-before-reap left the REAP itself
unbounded — a provider that answered every op then refused to exit on
EOF/shutdown would park the runtime thread forever on child.wait(), the
very hang the deadline exists to end, slipped past the read into the reap.
New reap_grouped() gives a brief grace then group-SIGKILLs an un-exiting
child; wired at all three sites (chain, rights, wallet shutdown).
- guardian F3 (P12): scoped the "every money/access boundary is bounded"
doc claim to the three provider conversations actually bounded, restored
the unix-only qualifier, and registered the access-path sidecar residual.
- guardian F4: added the live-kill ratchet the money-critical sign leg
lacked (a_hung_wallet_provider_is_killed_and_classified_pre_broadcast),
asserting bounded return + marker + end-to-end refund classification.
- guardian F5: a watchdog fire now poisons the session so no later leg runs
on the dead child (a fired-but-read-Ok race can no longer degrade a
provable refund to a stranded Indeterminate hold); deadline errors append
the underlying read error for diagnostics.
- guardian F6 / red-team F1: honesty comments — the marker names the
session's purpose (any pre-broadcast leg), the wallet read is now capped,
and a footgun note that a future human-consent approve must run OUTSIDE
this deadline.
- red-team F2: DeadlineWatchdog now disarms on Drop, so an un-disarmed
watchdog is a no-op rather than a latent full-deadline group-kill.
Gate: server lib 1263 (default) / 1264 (dev-modes), runtime 434, common 96 —
all green; every new ratchet returns bounded (~1s, not the stub's 300s).
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
…ng open/view
Close the last residual the Sprint 41 guardian left open: the access-path
sidecar helper reads (the media/object content authorities and the trustless
grant sidecar) were still unbounded, so a hung or hostile content-authority
subprocess could park a request thread forever. This wires those reads onto
the EXISTING shared capsule_watchdog module (no fourth copy of the ordering)
and adds the shared pieces they need:
- ReapGuard: a spawned child that is BOUNDED-reaped on drop unless disarmed,
so EVERY early return between spawn and the point the child is handed to
its owner reaps it (no leaked live process, no zombie).
- read_line_deadlined / read_open_deadlined: per-op VIEW reads use the short
shared deadline; content-OPEN descriptor reads use a new, longer
content_open_deadline (ELASTOS_CONTENT_OPEN_DEADLINE_SECS, default 120)
because a media open runs ffmpeg + a seal handshake before it answers.
- read_to_eof_deadlined: the ONE bounded one-shot read (the grant sidecar).
- ACCESS_DEADLINE_MARKER: a content-authority timeout is a DENY (fail-closed)
— the mirror of the rights-decide rule, NEVER a money decision (these sit
on the open/view paths, not the pay spine).
Applied to object_authority (both descriptor reads + the object/page VIEW
reads + grouped spawn + bounded Drop), media_authority (both descriptor reads
+ the segment VIEW read + grouped spawn + bounded Drop, its local reaper
replaced by the shared ReapGuard), and access_grant (the one-shot run_sidecar).
Council fold (principles-guardian + red-team, both SHIP-WITH-FIXES):
- RT-F1 (HIGH) / guardian-1: object_authority leaked a LIVE child on a
well-formed-but-incomplete descriptor (field-extraction `?` after a good
read). Closed by the shared ReapGuard — every early return now reaps.
- RT-F2 (MED-HIGH): access_grant leaked the child on the stdin-write (EPIPE)
path — the reap ran only after the read. Closed by the same guard.
- RT-F3 (MEDIUM): the RPC-tuned 30s deadline silently capped an ffmpeg open
(an availability cliff). Split into the separate content_open_deadline.
- RT-F4 / guardian-2: child_pid() returned 0 → a fire would kill(-0) the
gateway's OWN process group. ReapGuard::pid panics instead, and arm now
refuses to kill on pid 0.
- guardian-3: access_grant hand-rolled a fourth copy of the arm/read/disarm/
reap ordering. Factored into the shared read_to_eof_deadlined (which also
now carries the marker, closing the doc over-claim).
- guardian-4: reap_grouped returns the ExitStatus so the one-shot folds a
non-success exit into its error.
- guardian-5/6/7/8: fmt fix; a hung-VIEW-read ratchet (the per-op leg,
distinct from the open); the write-leg-is-sub-pipe-buffer note; the stale
reap-backstop comment corrected; the prepare/assemble spawn_blocking
follow-up registered in KNOWN_GAPS.
Ratchets (all bounded ~1-2s vs a 300s stub): a hung media/object authority and
a hung grant sidecar are each killed and DENIED, and a sidecar that answers the
descriptor then hangs on a VIEW read is separately bounded.
Docs: the S41 sidecar residual is now CLOSED in KNOWN_GAPS, DRM_MARKETPLACE_RAIL
and FLINT_MANDATE_ENGINE — every content-open/view + provider conversation the
pay/access pipeline traverses is bounded, reap included, kill unix-only.
Gate: server lib 1267 (default) / 1268 (dev-modes), runtime 434, common 96 —
all green; fmt + clippy clean on the touched files.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
…lassifier
The last soft spot in the money classifier: `drm_marketplace::is_pre_broadcast_refusal`
decided refund-vs-hold by matching SUBSTRINGS of `buy_access`'s error string —
provider-controlled text touching a money decision. This makes the decision a
TYPE, not a string.
- `buy_access` now returns `Result<BuyOutcome, BuyError>` where
`BuyError::{PreBroadcast, Indeterminate}` is decided BY CONSTRUCTION: each
error site is mapped by its position relative to the broadcast op. Every
`broadcast_signed_*`/`broadcast_mock` call and every post-broadcast op is
`.map_err(Indeterminate)`; every pre-send leg (wallet-not-linked, source,
sold-out, drift, the wallet SIGN leg, the chain PREPARE read, the missing-
signature precondition, the dev record) is `.map_err(PreBroadcast)`.
- `DrmSettleError::from_buy_error` maps the variant. The string classifier
(`is_pre_broadcast_refusal` + its `for_test` bridge + the pre-broadcast
sentinel list + the `chain-provider op failed` exclusion) is DELETED.
- There is NO `From<String> for BuyError`, so a bare `?` on a broadcast call
will not compile — a future refactor cannot silently mis-map a send error.
- Side benefit the type unlocks: a chain deadline on the PREPARE (read) leg is
now correctly refundable while the same marker on the SEND leg stays held —
a distinction the string could not draw (all deltas are safe-or-refund).
Council fold (principles-guardian SHIP-WITH-FIXES + red-team SHIP-CLEAN; the
red team confirmed the invariant — no provider input can refund a possibly-sent
tx — holds by construction):
- guardian F1 (must-fix, P12): rewrote three wallet_signer doc/test comments
that still pointed at the deleted classifier — the wallet marker is now
diagnostic text, classification is the call-site variant.
- guardian F2 (P12 honesty): the prepare-leg refinement is transitively
covered by the sign-leg call-site mapping; reworded KNOWN_GAPS so "Proven
by" no longer adjoins an unratcheted claim, and registered the dedicated
prepare-leg fixture as a follow-up.
- guardian F3: added ratchets for the two previously-untested variant sites —
a dev record failure types PreBroadcast (a behavior change the typing makes
correct) and a post-broadcast record failure types Indeterminate.
- guardian F4 (P16): documented the trusted-binary caveat on the PreBroadcast
variant (the runtime owns the only broadcast; env allowlist is the follow-up).
- red-team INFO-2: removed the dead `needs a signed transaction` phrase check.
Retired ratchet intents preserved and strengthened: the hostile-provider proof
is now `from_buy_error_classifies_by_variant_not_by_string` (an Indeterminate
error embedding EVERY sentinel + both deadline markers still holds) plus the
end-to-end `a_broadcast_op_error_types_the_buy_as_indeterminate_even_with_a_sentinel`.
Gate: server lib 1264 (default) / 1269 (dev-modes), runtime 434, common 96 —
all green; fmt + clippy clean.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
…RM 2d
The DRM confirmation reconciler used to identify its pending records by
sniffing `rail_note` for a `drm:tx=` prefix, so a hostile HTTP payment
endpoint whose Indeterminate body began `drm:tx=` could get its pending
polled/resolved by the DRM driver (council S35 red-team F5, fail-closed
direction but wrong). This replaces the note-sniff with a STRUCTURED tag.
- `PaymentRail::{Unknown, Http, Drm}` on `PaymentRecord`, stamped from the
paying provider at `begin_attempt` via a new REQUIRED `PaymentProvider::
rail()` method (no default — the tag is now a COMPILE-TIME property, the
S43 type-over-comment lesson). `DrmMarketplaceProvider::rail()==Drm`,
`HttpPaymentProvider::rail()==Http`.
- `reconcile_drm_confirmations` selects via `is_drm_pending`: Drm→ours,
Http→never (regardless of note), Unknown→bounded pre-S44 note fallback.
A positively-tagged Http pending — even one whose body a hostile endpoint
crafted to begin `drm:tx=` — is never polled by the DRM driver.
- Back-compat: `#[serde(default)]` ⇒ pre-S44 snapshots load as `Unknown` and
keep the note fallback so in-flight pendings reconcile across the upgrade;
a re-persisted legacy record gains the tag.
Plus docs/LIVE_BUY_RUNBOOK.md — the operator-driven procedure for a real
testnet buy (Grant→Act→Prove); deliberately OUT of the CI gate (the sandbox
has no funded wallet/network — the gate proves everything else via mocks).
Council fold (principles-guardian SHIP-WITH-FIXES + red-team SHIP-CLEAN; the
red team confirmed both money directions — no hostile HTTP pending is
DRM-resolvable, and no genuine DRM pending is stranded by mis-tagging, since a
money-bearing pending is un-reopenable so its tag is immutable):
- guardian F1 (ship-blocking, P12): fixed a KNOWN_GAPS self-contradiction
(the residual index said 2d open while the cell marked it CLOSED) + the
stale row title.
- guardian F2 / red-team F1: made `rail()` a REQUIRED trait method (removed
the default) so "every new record is positively tagged" holds by
construction, not by a doc comment — 7 impls updated.
- guardian F3 / red-team F5: ratcheted the reopen-adopts-rail behavior
(and that a money-bearing pending's tag is immutable ⇒ no stranding) +
an e2e assertion that the production pay closure stamps `Drm`.
- guardian F4 / red-team F2: a Drm-tagged pending with no `drm:tx=` note is
now visible (a one-time WARN) instead of silently folded into the
never-mining set; ratcheted (confirmer never called, money unchanged).
- red-team F4: documented the forward-incompatible-downgrade constraint on
`PaymentRail` (unknown rail string fails load fail-closed, no serde(other)).
- guardian F5/F6: qualified the S35 round-trip comment and marked
`seed_pendings` as deliberately exercising the legacy fallback.
Gate: server lib 1268 (default) / 1273 (dev-modes), runtime 434, common 96 —
all green; clippy clean on the touched regions.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
…chain scaffold
The live-money last mile, in two honest halves. The sandbox has no funded
wallet or network, so a real on-chain buy CANNOT run here — and nothing in
this change claims one did. What ships:
(A) GATE-RUNNABLE (runs every push) — two tests in `verify_receipt_cmd` prove a
receipt carrying a DRM settlement `rail_ref` in its signed `CapabilityUse`
is an admissible artifact through the ACTUAL standalone `verify-receipt`
CLI evaluator: written to disk as JSON, read back, verified AUTHENTIC
(exit 0) with the pinned signer, the `drm:tx=` reference surviving the
round-trip; and a byte-tampered settlement tx breaks the record-hash
recompute ⇒ INVALID (exit 1). This is the auditor's artifact path, beyond
the in-memory `verify_mandate_receipt` the S35 e2e already covers.
(B) NETWORK-GATED (operator/CI only) — a new `live-chain` cargo feature gates
`tests/live_drm_buy.rs`, an `#[ignore]`d integration test that drives the
real `ChainDrmMarketplace` (resolve → on-chain price gate → sign → broadcast
→ confirm) per docs/LIVE_BUY_RUNBOOK.md. Triple-fenced from the gate: the
whole file is `#![cfg(feature = "live-chain")]`, the fn is `#[ignore]`d, and
CI never enables the feature. It asserts the buy is HELD at broadcast
(`Indeterminate` with a real tx, never charged — the S35 invariant) then
confirms on-chain.
Council fold (both seats SHIP-WITH-FIXES; both verified by execution that the
gate tests are load-bearing, the live test cannot leak into the gate — proven
three ways — and NOTHING claims a live buy executed):
- red-team F1: aligned the gate fixture's emit shape to the real reconciler
(`Action::Execute` on `elastos://runtime/pay/<payee>`) and softened the
"exact shape" wording so the fixture doesn't over-claim production fidelity.
- both F2: reworded the live test's doc from "the whole stack / never refund
a sent tx" to the honest provider→chain leg it actually drives; the
ledger/reconcile/receipt legs are gate-proven against mocks + driven live by
the gateway per runbook §2.
- red-team F3: added a network-free CI `cargo check --features live-chain
--tests` step so a rename can't silently rot the operator's only live tool.
- guardian F1: assert `ELASTOS_DDRM_BUY_SIGN=wallet` up front (equally
mandatory with the empty subject) so a misconfig fails with the true cause.
- red-team F4: assert the rail_ref carries a real 0x+64-hex EVM tx before
polling, so a stub binary can't green the confirm loop on garbage.
- red-team F5: pin the tamper failure to `!hashes_ok` (not merely
`!structurally_valid`) — the mechanism is now a ratchet.
- guardian F3 / red-team F7: documented that a live re-run RE-SPENDS (settle
ignores the idempotency key; the standalone test bypasses ledger dedup) in
the runbook + the timeout assert, and that a `live-chain` build (pulls in
dev-modes) must never be reused as a release artifact.
- guardian F6: KNOWN_GAPS + runbook now state plainly this has NOT yet been
run against a live testnet in this repo (record the first run's tx hash).
Gate: server lib 1268 (default) / 1273 (dev-modes), bin 105 (+2 CLI ratchets),
runtime 434, common 96 — all green; live-chain test compiles + is correctly
ignored (0 run in the gate).
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
…ic delta) CI declares `cargo fmt --all -- --check` but the tree had drifted (344 diff sites across 24 files) — a gate that cannot pass is a lie the honesty regime cannot carry (S46 council Track B4). This is `cargo fmt --all` verbatim: purely mechanical reflow, zero semantic change, proven by the unchanged test counts on the follow-up commit's full gate. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
…leg ratchet, spawn_blocking)
Three council carry-overs closed, continuing the drive to 10/10 while the live
Base run waits for the operator:
- B1 (P16, council S43 guardian F4 partial): `capsule_watchdog::spawn_grouped`
— the single seam every provider/sidecar spawn goes through — now strips
RUNTIME_ONLY_SECRETS from the child env: ELASTOS_DDRM_BUY_SIGNED_TX (a
fully broadcastable signed tx; leaked to a hostile provider binary it could
be broadcast out-of-band while the read leg fails "pre-broadcast") and
ELASTOS_PAYMENT_TOKEN (the HTTP rail bearer). Both are consumed exclusively
in-process and passed onward as explicit op arguments, so no capsule has a
legitimate use for the env copy. Ratchet:
`runtime_only_secrets_are_stripped_from_spawned_capsules` (canary secrets
ABSENT in the child; ordinary ELASTOS_* config still passes through).
Honest scope: a targeted denylist of runtime-only secrets — the full
per-capsule env allowlist remains the stronger tracked hardening.
- B2 (closes council S43 guardian F2): the dedicated PREPARE-leg ratchet
`buy_authority::a_chain_prepare_deadline_types_the_buy_as_pre_broadcast` —
a chain stub answers the LISTING read then hangs only on
`prepare_transaction`; the buy is killed at the deadline and typed
`BuyError::PreBroadcast` (refund), carrying the SAME CHAIN_DEADLINE_MARKER
the SEND leg holds as Indeterminate — the call site decides, not the bytes.
Guards the ordering invariant a refactor could silently break (hoisting the
prepare out of the sign closure).
- B3 (closes council S42 guardian F8): `access_grant::prepare`/`assemble` in
viewer_open now run inside `tokio::task::spawn_blocking` — the
deadline-bounded (≤~31s) sidecar wait holds a blocking-pool thread, never
an async worker; a panicked task maps to the same fail-closed error arm.
- KNOWN_GAPS updated for all three closures (F4 marked PARTIALLY closed —
the allowlist stays tracked).
(The preceding commit 133c340 is the mechanical repo-wide `cargo fmt` that
makes CI's declared fmt gate truthful — B4.)
Gate: server lib 1269 (default) / 1275 (dev-modes), bin 105, runtime 434,
common 96 — all green; fmt 0 drift; clippy clean on touched files.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
…ard + gate the dev-modes lane
The S46 council (red-team SHIP-WITH-FIXES + guardian SHIP-WITH-FIXES) proved
three of the sprint's claims outran the code; this folds all of them:
- red-team F1 / guardian F1 (HIGH, the blocker): "single spawn seam" was
FALSE — `ProviderBridge::spawn` (every general provider capsule), the
carrier service spawn, and the shell-capsule spawns (main/serve_cmd)
bypassed `spawn_grouped` and inherited the rail bearer under a normal
HTTP-rail config. The strip now lives as ONE shared list
(`elastos_runtime::provider::RUNTIME_ONLY_SECRETS`) applied at ALL FIVE
capsule seams; host-tool spawns (git/tar/self re-exec, which legitimately
keeps its env) are deliberately not stripped.
- red-team F5 (MEDIUM): the canary proved the helper strips, not that spawns
route through it. Added the SOURCE-STRUCTURAL guard
`every_command_spawn_site_is_a_known_seam_and_capsule_seams_strip_secrets`:
every `Command::new` site in elastos-server/elastos-runtime must be a
classified capsule-seam-carrying-the-strip or a consciously allowlisted
host tool — a NEW spawn path fails the gate until classified.
- guardian F2 (MAJOR, P12): `assemble_cached` (the popup-free re-open path)
still shelled the grant sidecar on the async executor one line below the
fix — now also in spawn_blocking, preserving its fall-back-to-enrolled arm.
- guardian F3 (MAJOR, gate honesty): the dev-modes construction ratchets
(S43 typed BuyError, the S46 prepare-leg deadline) were NEVER COMPILED by
the gate. `just _verify-tail` and ci.yml now both run the
`--features dev-modes` lib lane — a ratchet outside the gate cannot ratchet.
- guardian F4 (MINOR, accepted): the new test's env hygiene matches its S43
siblings' pattern; adopting the RAII EnvGuard across the ddrm test family
is noted follow-up work, not folded here.
- Both seats verified the fmt commit (133c340) mechanically reproducible
bit-for-bit, B2 (prepare-leg ratchet) rigorous, and B3 semantics preserved.
- KNOWN_GAPS updated to the now-true wording (all seams named, the false
first-cut claim recorded, the structural guard cited).
Gate: server lib 1270 (default) / 1276 (dev-modes), bin 105, runtime 434,
common 96 — all green; fmt 0 drift.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
…ps (Track C1)
The media/object viewer session stores were unbounded HashMaps: an entry —
holding MB-scale init bytes, sealed material, and an Arc to a live
KEY-AUTHORITY SUBPROCESS — lived until an explicit client close. Sustained
opens grew memory and process count without limit; an abandoned tab pinned a
child process forever. Worse (S47 guardian F2): /init, /cover, and the
manifests had NO expiry check, so an expired session's bytes were still
served (only segment/bytes/page reads failed closed).
New `api/session_bounds.rs` — ONE discipline both stores apply:
- SWEEP: every admission and every lookup removes all EXPIRED entries.
- CAP: at MAX_VIEWER_SESSIONS (256/store) evict soonest-to-expire live
sessions (TTL order, not LRU — fixed-window sessions, no per-read
bookkeeping, ungameable by touching your own sessions) with a warn.
- DEFERRED-DROP CONTRACT (the council crux, both seats HIGH/MEDIUM): a
session drop can reap a subprocess with ~1s grace; the sweep runs under
the store Mutex, so dropping in place would stall EVERY viewer's lookup
for N×1s on a mass sweep (worst case ~4 min with 255 hung authorities).
The helpers RETURN removed values; call sites (put/get/remove, both
stores) drop them after releasing the lock.
- Fail-closed: an evicted session's next read is "no such session" — the
viewer re-opens through the FULL authorization gate. Eviction can never
grant access (red-team confirmed; in-flight reads finish on their own
Arc clone).
Council fold (both seats SHIP-WITH-FIXES; the blocker was pre-folded):
- F1 (HIGH): deferred off-lock drops — including the pre-existing
remove_*_session single-drop.
- G-F2 (P12): module docs now credit the lookup-sweep with closing the
previously-unchecked /init//cover/manifest expiry (the change undersold
itself).
- RT-F2/G-F3: the process-global, principal-blind cap + the authorized-
opener precondition + per-principal fairness as future work — documented.
- RT-F6/G-F4: honest worst-case math on the cap (≈2.3 GB + ~512 procs/store
fully adversarial, resting on MAX_CAPSULE_LINE) replaces "the host
survives".
- G-F5a: the wiring test now inserts the expired session LAST, so the
LOOKUP sweep is provably the remover.
- G-F5b: the object store's missing wiring test recorded as an honest gap
(non-optional Arc<ObjectAuthorityProc> holds a real Child; wiring is
byte-identical to the tested media store).
- Clock/concurrency/off-by-one all verified consistent by the red team
(sweep and route guards share the same predicate; no serve-after-sweep
window; a backward clock step keeps MORE, never mass-sweeps).
Gate: server lib 1275 (default) / 1281 (dev-modes), bin 105, runtime 434,
common 96 — all green; fmt 0 drift.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
…er contract (Track D1+D2)
The North Star breadth move: prove the market-provider seam is not DRM-shaped
by shipping a second chain-settled vertical behind it, and publish the
contract a third party could implement a provider from.
D1 — docs/SPEC-market-provider-v1.md: the versioned contract. The runtime owns
every money DECISION (mandate gate, meter, record-before-broadcast ledger,
receipts, reconciliation); a provider owns only the money REPORT (the
two-generals pay() classification + the compiler-required rail() tag + the
fail-safe confirmation reader). A conformance checklist maps each obligation
to its mechanical enforcement.
D2 — api/erc20_checkout.rs (`Erc20CheckoutProvider`): `runtime.pay
{payee: 0x…, amount}` becomes `transfer(payee, amount × ELASTOS_ERC20_SPEND_UNIT)`
on ONE operator-declared token:
- Typed by construction (the S43 discipline): bad address / unit overflow /
calldata / the wallet SIGN leg (incl. the chain PREPARE read) are
NotCharged — provably nothing moved; the broadcast op and after are
Indeterminate carrying `erc20:tx=<hash>;to=…;amount=…;tok=…`
(delimiter-stripped per component). A broadcast-accepted checkout is NEVER
charged until confirmed — Ok-at-broadcast does not exist.
- `rail() = PaymentRail::Erc20` (new variant; snake_case "erc20";
forward-compat posture per the S44 contract — a coordinated upgrade).
- Mock settlement: dev-modes + the explicit mock-money opt-in. Live: wallet
managed signing (dev-modes; a release build refuses NotCharged — the
external-signature checkout flow is a stated follow-up, same posture as
the DRM rail).
The chain-settled reconciler, generalized rail-STRICTLY:
- `parse_chain_tx(rail, note)`: Drm parses only `drm:tx=`, Erc20 only
`erc20:tx=` (a cross-rail note is a tx-less orphan — left Pending, warned,
never polled); the pre-S44 `Unknown` legacy fallback stays DRM-ONLY (it
never widens to new rails); Http never parses.
- `is_drm_pending` → `is_chain_settled_pending` ({Drm, Erc20} = ours);
ONE shared `confirm_chain_tx` (receipt + depth floor) both verticals use;
`Erc20CheckoutProvider` impls the confirmer via it, so the in-runtime
scheduler polls checkout pendings exactly like DRM buys.
Wiring — `ELASTOS_PAYMENT_RAIL=erc20` mirrors the DRM arm's fail-closed
discipline: ELASTOS_ERC20_TOKEN + ELASTOS_ERC20_SPEND_UNIT required (the cap
is a literal on-chain ceiling, never a silent 1-wei assumption); durable
meter/ledger required; mock mode refuses without ELASTOS_ALLOW_MOCK_PAYMENTS.
7 new ratchets: transfer-calldata words; junk payees refused pre-money;
unit-overflow refused pre-money; broadcast held-Indeterminate with a
parseable ref (e2e, dev-modes); Erc20 pendings reconciled + the S44 walls
hold for the new rail (hostile Http `erc20:tx=` never polled; Unknown legacy
stays DRM-only); parse_chain_tx rail-strict; the "erc20" serde round-trip.
Gate: server lib 1281 (default) / 1288 (dev-modes), bin 105, runtime 434,
common 96 — all green; fmt 0 drift. Council review in flight; findings fold
as a follow-up commit.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
…ot warn
Council S48 red-team returned SHIP-CLEAN (all five money invariants confirmed
by construction: no charge-at-broadcast, no refund-after-send, the cap is a
literal ceiling, cross-rail steering impossible, the payee cannot forge
calldata). Two LOW footguns folded:
- LOW-2: ELASTOS_ERC20_MODE parse is now case-insensitive AND an
unrecognized non-empty value REFUSES TO WIRE (fail-closed) rather than
silently defaulting to Live — a typo'd mode must be visible, never a
surprise settlement mode. Empty/absent still defaults Live (the stricter
path).
- LOW-1: a boot-time warn when mock mode wires — mock settlements produce
tx hashes the LIVE confirmer can never find, so mock pendings hold their
reservations until manually resolved (money-safe over-hold, stated).
INFO-1 (the wallet-capsule trust posture on the live sign leg) is inherited
from the DRM rail unchanged and already documented there.
Gate: check clean, fmt 0 drift, server lib default 1281 green.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
…r + wiring ratchet
Council S48 guardian returned SHIP-WITH-FIXES (the money core verified clean —
every leg typed by construction, no Ok path exists, the S44 walls hold and are
re-ratcheted for the new rail). All seven findings folded:
- F1 (HIGH, gate break): erc20_checkout's dev-modes-shadowed code left 5
clippy warnings in the default build, breaking the declared `-D warnings`
gate. Fixed with cfg-gated imports/helpers + a documented release-shape
dead_code allow. And went further (the S46 gate-truth rule): the WHOLE
workspace is now clippy-clean at 0 warnings — folded the pre-existing
drift too (wasm.rs too_many_arguments documented allow; session_bounds
deliberately-write-only field; parse_chain_tx lifetime elision;
intent_executor + bench doc-list indentation; market_quote's unit error
is now the named `ReadInFlight` type; carrier's 3 test env-lock guards
held across await get a justified allow — the lint targets production
deadlock risk, not test env serialization).
- F2 (MEDIUM, P11+P12): the rail selector is now CLOSED-WORLD — an
unrecognized non-empty ELASTOS_PAYMENT_RAIL refuses to wire (fail-closed)
instead of silently falling through to the HTTP/mock arms; the SPEC §6
"never degrades silently" sentence is now true of the selector itself.
- F3 (MEDIUM, P12): `erc20_rail_obeys_the_wiring_discipline` — the missing
wiring ratchet mirroring the DRM sibling: typo'd rail refuses; no token /
no unit / malformed unit / unrecognized mode / mock-without-opt-in all
refuse; fully-declared wires durable with the Erc20 tag + the confirmer.
(Takes the documented two-lock order — the first run caught an env race.)
- F4: SPEC row 4 reworded honestly (provider broadcast ratchet + the
shared-spine e2e via DRM, not a uniform "e2e").
- F5: the constructor's spend_unit clamp is now debug_assert'd + documented
(never silent).
- F6 (P12): wiring Live mode on a release build now warns at boot that
every checkout refuses until the external-signature flow ships.
- F7: SPEC §6 names the shared chain-config envs (the DDRM-era names; a
rail-neutral rename is tracked); the scheduler's "not the DRM rail"
doc/warn updated to "not a chain-settled rail".
Gate: server lib 1282 (default) / 1289 (dev-modes), bin 105, runtime 434,
common 96 — all green; fmt 0 drift; cargo clippy --workspace --all-targets:
ZERO warnings (the -D warnings gate passes truthfully).
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
…d by a conformance ratchet (Track D4)
docs/SPEC-mandate-v1.md: the byte-level formats behind Grant → Act → Prove,
written FROM the code so an independent party can implement verification with
no Flint code:
- §1 the trust model, stated without inflation (self-asserted signer — an
unpinned verification is NOT a trust decision; tamper-evident not
tamper-proof; the two completeness bounds).
- §2 the chained record + the exact hash preimage
(domain ‖ seq_be8 ‖ prev_hash32 ‖ event_json) + sig over the 32 RAW hash
bytes; the one canonicalization recipe (re-serialize the deserialized
event).
- §3 the receipt document; §4 the set-binding message byte layout (scope tag
byte, BE length prefixes, record hashes as 64-char ASCII hex).
- §5 the mandate events (INTERNALLY tagged via "type"; absent-when-unset
rules that keep old chains verifying; rail_ref formats cross-referenced to
SPEC-market-provider-v1).
- §6 the verification algorithm step by step + every verdict bit + the
fail-closed exit-code contract (0/1/3/4).
- §7 the signed intent + its preimage (domain WITH trailing NUL,
LITTLE-endian length prefixes — the endianness difference vs §2/§4 is
called out), and what the runtime enforces before executing.
- §8 versioning: frozen shapes, append-only optional fields, new tag for any
break.
The conformance ratchet `audit::tests::the_wire_format_matches_spec_mandate_v1`
pins the schema tag, both domain strings, every serialized key of the
document/record/scope/events, the timestamp shape, and that an exported
receipt verifies AUTHENTIC under §6. Writing it caught two real spec errors
before review (the event enum is internally tagged, and SecureTimestamp is
{unix_secs, monotonic_seq} — not the shapes first drafted), which is exactly
the failure mode the ratchet exists to prevent.
Gate: runtime 435 (+1), server 1282/1289, bin 105, common 96 — all green;
fmt 0 drift; workspace clippy 0 warnings. Council review in flight; findings
fold as a follow-up commit.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
…hets
Council: guardian SHIP-WITH-FIXES (3 HIGH P12 defects), red-team
SHIP-WITH-FIXES (the crypto core sound — 3 domains separated, scope-tag byte
defeats binding replay, no path lets a self-signed forgery reach exit 0 —
but the spec under-specified canonicalization and had two over-claims). All
folded, some in CODE:
- guardian F1 (HIGH): §5 listed a phantom `capsule_id` on capability_revoke;
the real event is exactly {type, timestamp, token_id, reason}. Fixed the
spec AND added a byte-identity fixture for revoke (and grant) to the
conformance ratchet — revoke was the one §5 event the ratchet hadn't pinned,
which is exactly how the error slipped in.
- guardian F2 (HIGH) / red-team domain check: §7 cited a ratchet that only
pinned the timestamp segment. Added the known-answer ratchet
`the_intent_preimage_digest_is_frozen` — the WHOLE preimage (trailing-NUL
domain, field order, little-endian prefixes) as one fixed digest — and the
spec now cites it truthfully.
- guardian F3 (HIGH): event byte-ORDER was neither specified nor pinned. §5
now gives byte-exact compact-JSON templates (type first, declared order,
escaping rules) and the ratchet pins grant+revoke bytes.
- red-team F3 (MEDIUM, CODE): the receipt verifier used non-strict ed25519
while the intent path uses verify_strict — two conforming verifiers could
disagree on a malleated signature. Switched both receipt sig checks to
verify_strict; the spec's Primitives now mandates strict verification.
- red-team F1/F2/F4/F5/F7 + guardian F4/F6/F9: canonicalization caveat on the
"no Flint code" claim; the contiguous-end-truncation exception stated in §1;
"a PRESENT set_binding MUST verify for both scopes" (the dangerous
accept-a-tamper reading closed); "verify over the RECOMPUTED hash, never the
claimed"; §7 qualified (agent-key binding only when bound; intent_id replay
key vs the flint-<sig> payment key); grant-position-free + set_binding null
accepted; lowercase-hex + length-not-the-separator domain notes.
Gate: runtime 436 (+1 KAT), server 1282/1289, bin 105, common 96 — all green;
fmt 0 drift; workspace clippy 0 warnings.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
Add the bounded-offer negotiation affordance: an agent under a pay-mandate makes an OFFER for a mandate-scoped asset and an injected Negotiator seller answers accept/counter/reject, closing the shop loop (quote -> negotiate -> pay). The provable property: the offer rides the signed input_hash as a canonical positive integer of spend units, and an offer above the mandate's un-spent cap (SpendMeter::remaining) is refused BEFORE it reaches the seller — an agent can never propose to commit its operator beyond granted authority. Non-value-moving by construction: it only reads the cap (no reserve, debit, or broadcast), so no money moves; settlement stays runtime.pay. A read-authority probe (like market_quote): it reconciles `read` and takes a read mandate on the pay resource — the dispatch gate authorizes only the fixed Action enum, so a made-up `negotiate` action could never be granted; the OFFER, not the action, makes it a proposal. The receipt attests the bounded offer, not the counterparty's disposition (which the runtime cannot verify); the seller's terms ride the response's disclosure channel as ephemeral market data. Production ListingNegotiator: a fixed-price DRM seller backed by the same read-only quote spine, reusing the buy gate's EXACT spend-unit conversion + pay-token guard (no second, divergent money-domain conversion). Wired only on the DRM rail (unwired on HTTP/ERC-20, which have no listing to negotiate against). Ratchet a_faithful_negotiate_returns_a_gate_legal_action_that_reconciles_matched pins the returned action to a gate-authorizable, declaration-matching value so the affordance can never regress to an action no token could authorize. Also fixes a stray SecureTimestamp clone the clippy -D warnings gate flagged in S49 test code. Full gate green: server lib 1300, dev-modes ok, bin 105, runtime 436, common 96; fmt clean; clippy -D warnings clean. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
…n + read-only seam contract Two-seat adversarial council: red-team SHIP-CLEAN (verified byte-identical buy-gate parity, cap-before-seller, non-value-moving, no report forgery); guardian SHIP-WITH-FIXES (3 MED + 2 LOW honesty/by-construction gaps). Both independently corroborated the pre-council action fix already landed. All folded: MED1 — the seller's rejection reason reaches the agent/logs but was unbounded (and embedded unsanitized chain-sourced token/price). The executor now bounds (<=200) and sanitizes (ascii-graphic + space) it, the same discipline the Performed report is held to. Ratchet a_hostile_seller_reject_reason_is_bounded_and_sanitized. MED2 — "reuses the buy gate's EXACT conversion" was by-comment, not by construction (a second, hand-synced copy). Extracted the ONE authorize_amount_against_listing (token guard + price parse + amount*spend_unit + cover boundary); both the DRM buy gate and the negotiate seller now call it, so an accept corresponds exactly to what the buy will pass. Buy-gate messages preserved (22 drm_marketplace tests green). MED3 — the read classification was honest only for the wired seller. Added a NORMATIVE contract to the Negotiator trait: implementations MUST be observationally read-only; an outbound-negotiating seller must reclassify the affordance action before being wired. LOW4 — the ratchet pinned a hand-copied action set. Added FromStr for Action (the one inverse of Display) and routed all three hand-rolled action matches (dispatch gate + two provisioning handlers) through parse_action; the ratchet now pins via FromStr. Round-trip test in token.rs. LOW5 — softened the panic-path comments to name the real residual: a seller that panics mid-read of the shared quote spine leaves a TTL-bounded (~30s) cache slot; money state stays clean and it is not agent-triggerable. Full gate green: server lib 1302, dev-modes 1309, bin 105, runtime 437, common 96; fmt clean; clippy -D warnings clean. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
…hanged (Track C2) MEASURE-FIRST. Baseline (benches/audit_emit.rs, this box): memory-only emit 2.2 us/op (~446k ops/s); file-backed fsync-per-record ~950 us/op — a ~1.1k emits/s durable ceiling at ~430x the CPU cost, with every concurrent emitter serialized behind it. That fsync-under-the-chain-lock is the KNOWN_GAPS G8 blocker keeping ordinary grant/use events best-effort. THE REWRITE (three measured cuts; the first two were rejected by their own numbers): emit now appends in chain order under the chain lock (seq + sign + ordered write + BufWriter flush), publishes written_seq, then WAITS until a flusher's sync_all covers its seq — N concurrent emits share one fsync. - Cut 1 anchored the tail-truncation head anchor under the flush lock: 1.5x. - Cut 2 fsynced through the writer mutex: appenders (holding chain, waiting on writer) convoyed behind every ~1 ms fsync — batches collapsed to ~1 and 8 threads measured SLOWER than serial (725 vs 1057 ops/s). Rejected. - Final: the flusher fsyncs a CLONED fd (fsync commits the inode, not the fd), so appends proceed during the fsync; the anchor is guarded-monotone on its own mutex off the flush path. 8 threads: 3116 emits/s — 2.9x past the single-writer ceiling (pre-S51 they were pinned AT it). Single-threaded latency unchanged (one fsync each; nothing to coalesce). THE CONTRACT IS UNCHANGED AND HARDENED: Ok(()) still means THIS record is durably fsynced. A failed write/flush/fsync now POISONS the log — the failing emit and every later one return Err (fail-closed) — which also retires the pre-S51 latent hazard where retrying a failed seq could append a duplicate seq behind half-landed bytes and corrupt the chain for verifiers. The head anchor only ever records durable covers (never over-claims). Ratchets: concurrent_emits_group_commit_and_the_chain_stays_perfect (8x25 threads -> contiguous, signed, verify_chain-clean, anchor at full count, re-opens with_file_verified) and a_failed_durable_write_poisons_the_log_ fail_closed. Bench gains an 8-thread regime that verifies the chain after timing, so its number counts correct commits only. KNOWN_GAPS G8/perf notes updated: fail-closed grant/use is now a policy decision, not a blocked one. Lanes green at commit: runtime lib 439 (all 50 audit), server lib 1302, bin 105, common 96, fmt clean. dev-modes + clippy tail and the two-seat council run next; their findings land in the fold commit. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
…usher unwind guard Two-seat adversarial council on the group-commit rewrite: both seats SHIP-WITH-FIXES, and both independently CONFIRMED the core invariants sound (Ok => durably fsynced; anchor never over-claims; flusher handoff liveness; Release/Acquire cover pairing; cloned-fd fsync semantics). All findings folded: F1 (guardian HIGH == red-team F2, the one real race): the poison gate was check-then-append — an emit queued on the chain lock during another emit's write failure could re-derive the FAILED seq and append behind its half-landed bytes (duplicate-seq corruption; fail-closed in every consequence but the chain bricked, and in the worst interleaving the racer's own record could be acknowledged on a never-verifiable chain). Closed by construction: the write-failure arm poisons while STILL HOLDING the chain lock, and emit re-checks poison UNDER the chain lock (chain->flush nesting verified acyclic). Ratchet: a_poisoned_log_refuses_before_assigning_a_seq_or_writing. F2 (both seats MED): "restart re-opens from the durable prefix" was claimed, not implemented — a crash-torn tail (provably never-acknowledged: an acknowledged record's full line+\n was fsynced) made the verified open REFUSE a healthy log, and S51's standing flushed-not-fsynced batch widened the window. Closed: quarantine_torn_tail at open moves a non-newline-terminated trailing fragment to <log>.torn-tail (preserved, never destroyed) and resumes from the intact prefix; safe because it cannot remove an acknowledged record and grants a tamperer nothing beyond the anchor floor they already had via a clean line-boundary cut. Mid-file corruption still refuses (tamper). Ratchet: a_torn_tail_is_quarantined_and_the_log_resumes_from_the_durable_prefix. F3 (guardian MED / red-team LOW): a panic-poisoned flush mutex could strand flushing=true (every later custody emit blocking forever — worse than fail-closed). All flush-lock sites now recover via PoisonError::into_inner (FlushState is single-statement-writes valid), a FlusherGuard clears+poisons on unwind, and the loud tracing moved outside the lock. F4 (guardian MED, resolved BY MEASUREMENT): the empty-anchor-after-crash brick is closed by treating an EMPTY anchor as absent (skip the floor for that open, loudly). The fold's first cut ALSO fsynced the anchor temp — and the bench rejected it: a single-threaded emitter is its own flusher, so it doubled the per-record cost (~950us -> ~1.7ms). Reverted with the honest rationale (the <=20-byte single-sector payload is complete-or-empty after a crash, never partial garbage; non-empty garbage stays fail-closed). F5 (guardian LOW): anchored_seq now seeds from max(resumed head, ON-DISK anchor) so an unverified reopen of a truncated log can never regress the anchor and destroy truncation evidence. F6 (guardian LOW): measured-number drift harmonized to the S51 decision run. RESIDUAL noted in KNOWN_GAPS: dual-writer same-file (no flock) is pre-existing; advisory flock is the tracked close. Post-fold measurements (uncontended): single-writer ~874 us/op (~1.1k/s, unchanged from pre-fold); 8-thread group commit 2947 emits/s (2.6x the single-writer ceiling). Full gate green: runtime 441 (52 audit), server 1302, dev-modes 1309, bin 105, common 96; fmt clean; clippy -D warnings clean. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
…acket Flint scope (a) AuditLog::with_file now holds an exclusive advisory flock on <log>.lock for the log's lifetime — the SAME single-opener discipline the spend meter and payment ledger hold (council S28 F4 lineage), closing the S51 red-team dual-writer residual on the LAST unprotected durable custody store. Two live instances on one file would keep independent chain heads, mint the same seqs, and corrupt the on-disk chain (and under the S51 group commit one instance's fsync covers the sibling's uncoordinated bytes); a second opener now fails WouldBlock fail-closed. Taken BEFORE the torn-tail quarantine so racing openers cannot both mutate the file. Ratchet: a_second_live_opener_of_the_same_log_is_refused (refusal + verified-open refusal + reopen-after-drop). The gateway's lazy audit-log constructor now serializes construction (its benign both-construct race would otherwise surface a spurious flock refusal); server_infra distinguishes the already-open-elsewhere refusal from tamper in its boot error (council LOW2). (b) The dev-only echo_stdout println moved OUTSIDE the chain lock (S51 red-team F4) — a blocked stdout can no longer serialize every emit — and now echoes only records that actually committed. (c) docs/AUDITOR_PACKET.md gains section 7: the Flint mandate/money plane as the second external-audit scope (the four invariant families to attack, the two wire-format specs, reproduction commands, and the honest note that the internal two-seat council reduces engagement cost but does not replace independent assurance). Deliberately NOT touched: live-buy env wiring (cosmetic renames deferred until after the operator's real-transaction test). Council (combined seat): SHIP-CLEAN — drop-order (lock releases after the writer flushes), O_CLOEXEC fd non-inheritance, the no-real-dual-opener sweep (CLI paths never open the live log file), double-checked OnceLock init, and the packet's invariant claims all verified against source; both LOW honesty notes folded (Unix-only qualifiers; the boot-error branch). Full gate green: runtime 442 (53 audit), server 1302, dev-modes 1309, bin 105, common 96; fmt clean; clippy -D warnings clean. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this delivers
This branch has grown well beyond its original scope. It now delivers, in one reviewable arc into
flint-0.5(35 commits, 43 files, +8.6k lines):1. The Flint mandate engine — "give your agent a mandate, not your keys"
The layer that lets an operator hand an AI agent a scoped, expiring, revocable mandate instead of raw credentials, have the agent act under it unsupervised, and hold the whole thing to a tamper-evident, off-box-verifiable record. Live end-to-end in the CLI and the ElastOS Mandates shell app:
Grant → Act → Watch → Revoke → Prove.
IntentDeclarationV1and dispatches it through a fail-closed gate (intent ⊆ envelope: capsule + agent-key + method + resource + action); a real executor performs it; the runtime reconciles declared-vs-done. Receipts are minted from what the executor reports, never the declaration — so an authorized-but-unperformed act reconciles honestly (authorized_not_performed), never a fabricated match.runtime.audit_verify,runtime.content_seen) and two side-effecting (runtime.notify→ operator Inbox;runtime.state_put→ durable, readable-back, principal-scoped agent state).MandateReceiptand verify it off-box withelastos verify-receipt— no runtime, no trust in this box.Gaps: closed — G-M1, G-M2, G-M5, G-M6. Open/tracked with documented reasons in
docs/KNOWN_GAPS.md— G-M3 (shell is the grant root, accepted), G-M4 (agent-key binding optional), G-M7 (operational hardening; replay guard paid down, rate-limiting + role-tiering remain).Review discipline: every sprint ran gate (build + clippy + full suites) → principles-guardian → red-team → fold findings with a ratchet reproducing the exact failure → commit. Nothing dismissed. Council catches that were fixed: a cross-principal content oracle, a receipt over-claim, a wrong-target kill-switch race, an operator-Inbox phishing channel, and a backward-clock replay regression (plus its persist-failure sub-case).
Reviewer's map:
docs/FLINT_MANDATE_ENGINE.md→intent.rs(gate + store + replay guard) ·intent_executor.rs(affordances) ·handlers/capability.rs(issue/revoke/dispatch/receipt) ·api/gateway_mandates.rs(shell surface) ·capsules/mandates/index.html(UI) ·docs/KNOWN_GAPS.md(gap ledger).2. MKT-1 closed — money-path security fix (the original HIGH)
The KID→ledger-tokenId resolver could mis-bind a buyer to a hostile co-channel mint (on-chain-reachable). Closed with fail-closed global uniqueness: takes the union of both bind methods (
abi.rs), accumulates every distinct(operative, tokenId)across the whole channel range and binds only if exactly one exists elseambiguous_kid_binding(main.rs), and aborts on un-fetchable calldata rather than deciding on a partial set. Deliberate tradeoff: availability for safety on a money path. Red-teamed — all six mis-bind vectors CONFIRMED-SAFE; three ratchet tests; KNOWN_GAPS MKT-1 → CLOSED.3. The 0.01% audit
docs/AUDIT_2026-07-03.md— six-seat adversarial audit with a reconciled grade card (≈7/10; security 8, crypto 8, arch 7, code 7, product 6, perf 5).docs/INVESTOR_ONE_PAGER.md— the honest external framing.Verification at merge
cargo clippyclean acrosselastos-runtime,elastos-server,elastos-common.todo!()/unimplemented!()in the mandate path; every closed gap carries a ratchet, every open gap a documented reason.🤖 Generated with Claude Code
Generated by Claude Code